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
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { existsSync } from "node:fs";
4
4
  import { EventEmitter } from "node:events";
5
5
  import { PassThrough } from "node:stream";
6
- import { chmod, copyFile, mkdtemp, mkdir, readFile, readdir, rename, stat, symlink, writeFile } from "node:fs/promises";
6
+ import { chmod, copyFile, mkdtemp, mkdir, readFile, readdir, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { dirname, join } from "node:path";
9
9
  import test from "node:test";
@@ -13,6 +13,8 @@ import {
13
13
  INSTALLER_VERSION,
14
14
  GENTLE_AI_RELEASE_ASSETS,
15
15
  GENTLE_AI_WINDOWS_SOURCE_MODULE,
16
+ GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH,
17
+ GENTLE_AI_WINDOWS_SOURCE_TAG,
16
18
  GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
17
19
  downloadGentleAiAsset,
18
20
  gentleAiAssetForm,
@@ -22,20 +24,33 @@ import {
22
24
  trustedSystemExtractor,
23
25
  } from "../scripts/gentle-ai-installer.mjs";
24
26
 
25
- // v4.0.0 archive digests independently match the minisign-signed release
27
+ // v4.1.1 archive digests independently match the minisign-signed release
26
28
  // checksums; binary digests were computed from the extracted executables.
29
+ function releaseAssets(): Record<string, { name: string; sha256: string; binarySha256: string; url: string }> {
30
+ const assets: Record<string, { name: string; sha256: string; binarySha256: string; url: string }> = {};
31
+ for (const [key, value] of Object.entries(GENTLE_AI_RELEASE_ASSETS)) {
32
+ assert.ok(typeof value === "object" && value !== null);
33
+ assert.ok("name" in value && typeof value.name === "string");
34
+ assert.ok("sha256" in value && typeof value.sha256 === "string");
35
+ assert.ok("binarySha256" in value && typeof value.binarySha256 === "string");
36
+ assert.ok("url" in value && typeof value.url === "string");
37
+ assets[key] = { name: value.name, sha256: value.sha256, binarySha256: value.binarySha256, url: value.url };
38
+ }
39
+ return assets;
40
+ }
41
+
27
42
  const EXPECTED_ASSETS = {
28
- "darwin/amd64": { name: "gentle-ai_4.0.0_darwin_amd64.tar.gz", sha256: "b5b74f22b38ec3339b38e8c68f797dc76ff12ed6580826a6e425d5c718da80c1", binarySha256: "d4a5b16ff70e65331e17a62356941bb0c75ecb9dbe3a0d98a6b54cfbd76cd6b0" },
29
- "darwin/arm64": { name: "gentle-ai_4.0.0_darwin_arm64.tar.gz", sha256: "d2159caf6d68f367b18830ece6af71ef26963d5f5320d7df6a794773f45cc7e9", binarySha256: "18a9f7fae55d85c95684b6d512a4a148d0cb24a856325f72573c34caf65159eb" },
30
- "linux/amd64": { name: "gentle-ai_4.0.0_linux_amd64.tar.gz", sha256: "5f4417cf29c969c86da4799942fd673368840901be1bb09c779a12d7ed6096ea", binarySha256: "50ba217b5138c1a9c7d5bf2f79931b1bb89b89c4cf650dcd7ee037657c88158d" },
31
- "linux/arm64": { name: "gentle-ai_4.0.0_linux_arm64.tar.gz", sha256: "1383b040c95cfc69206660d73c21907b14ad70ab913f410917c54f43d3147e57", binarySha256: "6703704f0c4a5b70c36fbdc44db641e810871cab16bc28040d06aa1d10704ad3" },
43
+ "darwin/amd64": { name: "gentle-ai_4.1.1_darwin_amd64.tar.gz", sha256: "f2e9aebfba3056e88a8d7c7193e4e8c04282dc126d5eaa2216bc807deadecffd", binarySha256: "82351dd86bf7323c6a5b9e9e68b3cc2893a65a4df04fbdf1ef1801cabf5d2338" },
44
+ "darwin/arm64": { name: "gentle-ai_4.1.1_darwin_arm64.tar.gz", sha256: "8093dced37130cb26534d81663013f81d9039342e33f00b8c85aa5319aa7eea9", binarySha256: "6362e3641b87ae2b41a8dd7695dad4cf47457ee86c9f63792e474476bfdb6b6a" },
45
+ "linux/amd64": { name: "gentle-ai_4.1.1_linux_amd64.tar.gz", sha256: "57869ebcdba5ba9942493ab413bbc15d146e2451c4dca3eaafcf163fcbd646b4", binarySha256: "7c638c8467efc7036e0ec6b8a1d9080a5e9a64f7ae6cbc6074fce577f789b7fe" },
46
+ "linux/arm64": { name: "gentle-ai_4.1.1_linux_arm64.tar.gz", sha256: "5f8fa38573eea0f2110902f0996aa0b008aac58dc0f9a6abbdf301dcbc93f4eb", binarySha256: "76a36a73d6905a16221496ffe9a193bd79e17951463d711cd03abf370349ef55" },
32
47
  } as const;
33
48
 
34
- test("published Gentle AI v4.0.0 is the installer pin", () => {
35
- assert.equal(INSTALLER_VERSION, "4.0.0");
49
+ test("published Gentle AI v4.1.1 is the installer pin", () => {
50
+ assert.equal(INSTALLER_VERSION, "4.1.1");
36
51
  // v4.0.0 moved the Go module path to the /v4 major-version suffix.
37
52
  assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE, "github.com/gentleman-programming/gentle-ai/v4");
38
- assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:pZ/XZ2Pk3U9lgXigOTY62zlxxFOHnc9CjQhLgaV/Hfc=");
53
+ assert.equal(GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM, "h1:+WmZ2IvIOiJ8qtHa7wsjnYKNasem+PByeOZ6HIjoAL8=");
39
54
  });
40
55
 
41
56
  test("default installer package root is the package containing scripts, not its parent", () => {
@@ -46,15 +61,15 @@ test("default installer package root is the package containing scripts, not its
46
61
  assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
47
62
  });
48
63
 
49
- test("release mapping selects only the supported official v4.0.0 assets and pinned digests", () => {
64
+ test("release mapping selects only the supported official v4.1.1 assets and pinned digests", () => {
50
65
  assert.deepEqual(
51
- Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
66
+ Object.fromEntries(Object.entries(releaseAssets()).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
52
67
  EXPECTED_ASSETS,
53
68
  );
54
- assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.0.0_linux_amd64.tar.gz");
55
- assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.0.0_darwin_arm64.tar.gz");
56
- for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
57
- assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.0\.0\//);
69
+ assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_4.1.1_linux_amd64.tar.gz");
70
+ assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_4.1.1_darwin_arm64.tar.gz");
71
+ for (const asset of Object.values(releaseAssets())) {
72
+ assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v4\.1\.1\//);
58
73
  }
59
74
  });
60
75
 
@@ -64,18 +79,18 @@ test("raw release assets are admitted only under a prerelease pin", () => {
64
79
  assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
65
80
  // A raw binary under a stable pin means the pin itself is wrong: stable
66
81
  // releases publish signed archives only, so this fails closed pre-download.
67
- assert.throws(() => gentleAiAssetForm("gentle-ai_4.0.0_linux_amd64", "4.0.0"), /only admitted for a prerelease pin/);
82
+ assert.throws(() => gentleAiAssetForm("gentle-ai_4.1.1_linux_amd64", "4.1.1"), /only admitted for a prerelease pin/);
68
83
  assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
69
84
  // The current stable pin admits every pinned asset row through the same
70
85
  // gate the installer uses at download time (default installerVersion
71
86
  // argument): signed archives, never raw binaries.
72
- for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
87
+ for (const asset of Object.values(releaseAssets())) {
73
88
  assert.equal(gentleAiAssetForm(asset.name), "archive");
74
89
  }
75
90
  });
76
91
 
77
92
  test("release digests are all-or-none and install fails closed while any digest is pending", async () => {
78
- const digests = Object.values(GENTLE_AI_RELEASE_ASSETS).flatMap((asset) => [asset.sha256, asset.binarySha256]);
93
+ const digests = Object.values(releaseAssets()).flatMap((asset) => [asset.sha256, asset.binarySha256]);
79
94
  const pinned = digests.filter((digest) => /^[0-9a-f]{64}$/.test(digest));
80
95
  const pending = digests.filter((digest) => digest === GENTLE_AI_PENDING_DIGEST);
81
96
  assert.equal(pinned.length + pending.length, digests.length, "every digest must be pinned hex or the explicit pending sentinel");
@@ -91,7 +106,7 @@ test("release digests are all-or-none and install fails closed while any digest
91
106
  }),
92
107
  /checksum mismatch/,
93
108
  );
94
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
109
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
95
110
  }
96
111
  });
97
112
 
@@ -99,6 +114,7 @@ interface WindowsGoCall {
99
114
  file: string;
100
115
  arguments_: string[];
101
116
  options: {
117
+ cwd?: string;
102
118
  env?: NodeJS.ProcessEnv;
103
119
  shell?: boolean;
104
120
  timeout?: number;
@@ -120,7 +136,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
120
136
  const metadata = [
121
137
  "gentle-ai.exe: go1.25.10",
122
138
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
123
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.0.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
139
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.1.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
124
140
  "\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
125
141
  ].join("\n");
126
142
  const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
@@ -138,7 +154,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
138
154
  return { stdout: "", stderr: "" };
139
155
  }
140
156
  if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
141
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.0.0\n", stderr: "" };
157
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 4.1.1\n", stderr: "" };
142
158
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
143
159
  };
144
160
  return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
@@ -154,7 +170,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
154
170
  const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
155
171
  assert.equal(result.installed, true);
156
172
  assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
157
- ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.0.0"], ["version", "-m"],
173
+ ["version"], ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.1.1"], ["version", "-m"],
158
174
  ]);
159
175
  }
160
176
  });
@@ -173,11 +189,60 @@ test("Windows source install reports missing or too-old Go without publishing a
173
189
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
174
190
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
175
191
  );
176
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai.exe")), false);
192
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), false);
177
193
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
178
194
  }
179
195
  });
180
196
 
197
+ test("a failed Windows go install keeps the go command's error, with its stdout and stderr, as the cause", async () => {
198
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-cause-"));
199
+ try {
200
+ const installError = Object.assign(new Error("Command failed: go.exe install\ngo: verifying module: checksum mismatch"),
201
+ { code: 1, stdout: "", stderr: "go: verifying module: checksum mismatch\n" });
202
+ const fixture = windowsGoFixture({ installError });
203
+ const goPath = join(packageRoot, "go.exe");
204
+ await writeFile(goPath, "trusted local Go executable");
205
+ fixture.setGoExecutable(goPath);
206
+ await assert.rejects(
207
+ () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
208
+ (error: unknown) => error instanceof Error && error.cause === installError && (error.cause as { stderr: string }).stderr.includes("checksum mismatch"),
209
+ );
210
+ } finally { await rm(packageRoot, { recursive: true, force: true }); }
211
+ });
212
+
213
+ // Run 38063142924: from a 179-character pnpm store package root, Go's asm.exe failed with
214
+ // "The directory name is invalid." because its working directory, inside GOMODCACHE, exceeded MAX_PATH.
215
+ test("Windows source install builds in a short private directory under the temp root, never inside the package, and removes it", async () => {
216
+ for (const installError of [undefined, Object.assign(new Error("go install failed"), { code: 1, stderr: "go: failed\n" })]) {
217
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-short-build-"));
218
+ const temporaryDirectory = await mkdtemp(join(tmpdir(), "gentle-pi-installer-temp-"));
219
+ try {
220
+ const fixture = windowsGoFixture({ installError });
221
+ const goPath = join(packageRoot, "go.exe");
222
+ await writeFile(goPath, "trusted local Go executable");
223
+ fixture.setGoExecutable(goPath);
224
+ const install = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath, temporaryDirectory });
225
+ if (installError) await assert.rejects(install, (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED");
226
+ else assert.equal((await install).installed, true);
227
+ const goInstall = fixture.calls.find((call) => call.arguments_[0] === "install");
228
+ assert.ok(goInstall);
229
+ const buildDirectory = goInstall.options.cwd ?? "";
230
+ assert.match(buildDirectory.slice(temporaryDirectory.length), /^[\\/]gai-[^\\/]+$/, "a fresh gai- directory directly under the temp root");
231
+ const env = goInstall.options.env ?? {};
232
+ for (const key of ["GOBIN", "GOPATH", "GOMODCACHE", "GOCACHE", "TEMP", "TMP"]) {
233
+ assert.ok(env[key]?.startsWith(buildDirectory), `${key} is under the build directory`);
234
+ assert.equal(env[key]?.startsWith(packageRoot), false, `${key} is outside the package`);
235
+ }
236
+ for (const call of fixture.calls.filter((entry) => entry.file === goPath)) assert.ok(call.options.cwd?.startsWith(buildDirectory), call.arguments_.join(" "));
237
+ assert.deepEqual(await readdir(temporaryDirectory), [], "the build directory is removed");
238
+ if (!installError) assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), true, "the verified binary is published into the package");
239
+ } finally {
240
+ await rm(packageRoot, { recursive: true, force: true });
241
+ await rm(temporaryDirectory, { recursive: true, force: true });
242
+ }
243
+ }
244
+ });
245
+
181
246
  test("Windows source install cleans staging after Go failure or wrong built version", async () => {
182
247
  for (const fixtureOptions of [
183
248
  { installError: Object.assign(new Error("go install timed out"), { code: "ETIMEDOUT" }), expectedCode: "GENTLE_AI_GO_INSTALL_FAILED" },
@@ -192,7 +257,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
192
257
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
193
258
  (error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
194
259
  );
195
- const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.0.0");
260
+ const runtimeDirectory = join(packageRoot, ".gentle-ai", "v4.1.1");
196
261
  assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
197
262
  assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
198
263
  assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
@@ -213,7 +278,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
213
278
  assert.equal(reused.installed, false);
214
279
  assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
215
280
 
216
- await writeFile(join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json"), "{}\n");
281
+ await writeFile(join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json"), "{}\n");
217
282
  const repaired = windowsGoFixture();
218
283
  repaired.setGoExecutable(goPath);
219
284
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
@@ -255,7 +320,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
255
320
  const metadata = fixtureOptions.metadataOverride ?? [
256
321
  "gentle-ai.exe: go1.25.10",
257
322
  "\tpath\tgithub.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
258
- `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.0.0\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
323
+ `\tmod\tgithub.com/gentleman-programming/gentle-ai/v4\tv4.1.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
259
324
  "\tbuild\t-buildmode=exe",
260
325
  "\tbuild\t-compiler=gc",
261
326
  "\tbuild\tCGO_ENABLED=0",
@@ -275,7 +340,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
275
340
  return { stdout: "", stderr: "" };
276
341
  }
277
342
  if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
278
- if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.0.0\n", stderr: "" };
343
+ if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 4.1.1\n", stderr: "" };
279
344
  throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
280
345
  };
281
346
  return {
@@ -317,7 +382,7 @@ test("Windows source installation resolves one validated Go executable and seals
317
382
  const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
318
383
  assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
319
384
  ["version"],
320
- ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.0.0"],
385
+ ["install", "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@v4.1.1"],
321
386
  ["version", "-m"],
322
387
  ]);
323
388
  for (const call of goCalls) {
@@ -343,14 +408,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
343
408
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
344
409
  const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
345
410
  const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
346
- const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json"), "utf8")) as Record<string, string>;
411
+ const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json"), "utf8")) as Record<string, string>;
347
412
  assert.equal(result.installed, true);
348
413
  assert.deepEqual(manifest, {
349
- version: "4.0.0",
414
+ version: "4.1.1",
350
415
  method: "go-sumdb-source-build",
351
416
  package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
352
417
  module: "github.com/gentleman-programming/gentle-ai/v4",
353
- tag: "v4.0.0",
418
+ tag: "v4.1.1",
354
419
  architecture: "arm64",
355
420
  binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
356
421
  moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
@@ -370,12 +435,12 @@ test("Windows source installation rejects Go metadata for a different architectu
370
435
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
371
436
  (error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
372
437
  );
373
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai.exe")), false);
438
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai.exe")), false);
374
439
  });
375
440
 
376
441
  test("Windows source installation treats a fresh ownerless lock as active", async () => {
377
442
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
378
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
443
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
379
444
  await mkdir(lockPath, { recursive: true });
380
445
  const fixture = await hardenedWindowsGoFixture(packageRoot);
381
446
  await assertManualLockRecoveryRequired(packageRoot, fixture);
@@ -383,7 +448,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
383
448
 
384
449
  test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
385
450
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
386
- const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock", "owner.json");
451
+ const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock", "owner.json");
387
452
  const fixture = await hardenedWindowsGoFixture(packageRoot);
388
453
  let replacedOwner = false;
389
454
  const run = async (...arguments_: Parameters<typeof fixture.run>) => {
@@ -400,7 +465,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
400
465
  });
401
466
 
402
467
  async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
403
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
468
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
404
469
  await assert.rejects(
405
470
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
406
471
  (error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
@@ -410,18 +475,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
410
475
  }
411
476
 
412
477
  function tombstonePath(packageRoot: string, nonce: string): string {
413
- return join(packageRoot, ".gentle-ai", `.v4.0.0.install.tombstone-${nonce}`);
478
+ return join(packageRoot, ".gentle-ai", `.v4.1.1.install.tombstone-${nonce}`);
414
479
  }
415
480
 
416
481
  async function tombstones(packageRoot: string): Promise<string[]> {
417
482
  const runtimeRoot = join(packageRoot, ".gentle-ai");
418
483
  return existsSync(runtimeRoot)
419
- ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.0.0.install.tombstone-"))
484
+ ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v4.1.1.install.tombstone-"))
420
485
  : [];
421
486
  }
422
487
 
423
488
  function backupBundlePath(packageRoot: string, nonce: string): string {
424
- return join(packageRoot, ".gentle-ai", `.v4.0.0.backup-${nonce}`);
489
+ return join(packageRoot, ".gentle-ai", `.v4.1.1.backup-${nonce}`);
425
490
  }
426
491
 
427
492
  async function copyWindowsBundle(source: string, destination: string): Promise<void> {
@@ -432,7 +497,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
432
497
 
433
498
  test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
434
499
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
435
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
500
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
436
501
  await mkdir(lockPath, { recursive: true });
437
502
  const fixture = await hardenedWindowsGoFixture(packageRoot);
438
503
  await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
@@ -440,7 +505,7 @@ test("Windows source installation fails closed for an ownerless lock even after
440
505
 
441
506
  test("Windows source installation fails closed for a stale owner lock", async () => {
442
507
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
443
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
508
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
444
509
  await mkdir(lockPath, { recursive: true });
445
510
  await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
446
511
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -492,7 +557,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
492
557
  (error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
493
558
  );
494
559
  assert.equal(existsSync(foreignTombstone), true);
495
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock")), false);
560
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock")), false);
496
561
  assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
497
562
  });
498
563
 
@@ -500,7 +565,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
500
565
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
501
566
  const fixture = await hardenedWindowsGoFixture(packageRoot);
502
567
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
503
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock")), false);
568
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock")), false);
504
569
  assert.deepEqual(await tombstones(packageRoot), []);
505
570
  const reuse = await hardenedWindowsGoFixture(packageRoot);
506
571
  assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
@@ -510,7 +575,7 @@ test("Windows source recovers a valid backup after a crash between publication r
510
575
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
511
576
  const initial = await hardenedWindowsGoFixture(packageRoot);
512
577
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
513
- const live = join(packageRoot, ".gentle-ai", "v4.0.0");
578
+ const live = join(packageRoot, ".gentle-ai", "v4.1.1");
514
579
  const backup = backupBundlePath(packageRoot, "crash");
515
580
  await rename(live, backup);
516
581
  const recovery = await hardenedWindowsGoFixture(packageRoot);
@@ -546,7 +611,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
546
611
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
547
612
  const initial = await hardenedWindowsGoFixture(packageRoot);
548
613
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
549
- const live = join(packageRoot, ".gentle-ai", "v4.0.0");
614
+ const live = join(packageRoot, ".gentle-ai", "v4.1.1");
550
615
  const backup = backupBundlePath(packageRoot, "valid");
551
616
  await copyWindowsBundle(live, backup);
552
617
  const reuse = await hardenedWindowsGoFixture(packageRoot);
@@ -559,7 +624,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
559
624
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
560
625
  const initial = await hardenedWindowsGoFixture(packageRoot);
561
626
  await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
562
- const live = join(packageRoot, ".gentle-ai", "v4.0.0");
627
+ const live = join(packageRoot, ".gentle-ai", "v4.1.1");
563
628
  const backup = backupBundlePath(packageRoot, "valid");
564
629
  await copyWindowsBundle(live, backup);
565
630
  await writeFile(join(live, "integrity.json"), "{}\n");
@@ -577,7 +642,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
577
642
  const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
578
643
  const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
579
644
  await fixture.waitForInstall();
580
- const lockPath = join(packageRoot, ".gentle-ai", ".v4.0.0.install.lock");
645
+ const lockPath = join(packageRoot, ".gentle-ai", ".v4.1.1.install.lock");
581
646
  await assert.rejects(
582
647
  () => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
583
648
  (error: unknown) => error instanceof Error && error.message.includes(lockPath),
@@ -591,7 +656,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
591
656
 
592
657
  test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
593
658
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
594
- const versionDirectory = join(packageRoot, ".gentle-ai", "v4.0.0");
659
+ const versionDirectory = join(packageRoot, ".gentle-ai", "v4.1.1");
595
660
  await mkdir(versionDirectory, { recursive: true });
596
661
  await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
597
662
  const fixture = await hardenedWindowsGoFixture(packageRoot);
@@ -612,6 +677,405 @@ test("Windows source publication rolls back a prior bundle when final directory
612
677
  assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle");
613
678
  });
614
679
 
680
+ function codedError(code: string, message: string) {
681
+ return Object.assign(new Error(message), { code });
682
+ }
683
+
684
+ function expectedWindowsSourceManifest(architecture: "x64" | "arm64") {
685
+ return {
686
+ version: INSTALLER_VERSION,
687
+ method: "go-sumdb-source-build",
688
+ package: GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH,
689
+ module: GENTLE_AI_WINDOWS_SOURCE_MODULE,
690
+ tag: GENTLE_AI_WINDOWS_SOURCE_TAG,
691
+ architecture,
692
+ binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
693
+ moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
694
+ goVersion: "go1.25.10",
695
+ goos: "windows",
696
+ goarch: architecture === "x64" ? "amd64" : "arm64",
697
+ buildMode: "exe",
698
+ compiler: "gc",
699
+ cgoEnabled: "0",
700
+ };
701
+ }
702
+
703
+ async function publicationLeftovers(packageRoot: string) {
704
+ const runtimeRoot = join(packageRoot, ".gentle-ai");
705
+ return existsSync(runtimeRoot)
706
+ ? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.staging-`) || entry.startsWith(`.v${INSTALLER_VERSION}.backup-`) || entry.startsWith(`.v${INSTALLER_VERSION}.install.`))
707
+ : [];
708
+ }
709
+
710
+ async function waitForCount(getCount: () => number, expected: number, label: string) {
711
+ const deadline = Date.now() + 5_000;
712
+ while (Date.now() < deadline) {
713
+ if (getCount() >= expected) return;
714
+ await new Promise<void>((resolve) => setImmediate(resolve));
715
+ }
716
+ throw new Error(`timed out waiting for ${label} (${getCount()} < ${expected})`);
717
+ }
718
+
719
+ async function settleWithMockedTimers<T>(t: { mock: { timers: { tick(milliseconds: number): void } } }, operation: Promise<T>): Promise<T> {
720
+ let done = false;
721
+ let value: T | undefined;
722
+ let failure: { error: unknown } | undefined;
723
+ operation.then(
724
+ (resolved) => { value = resolved; done = true; },
725
+ (error) => { failure = { error }; done = true; },
726
+ );
727
+ const deadline = Date.now() + 5_000;
728
+ while (!done && Date.now() < deadline) {
729
+ await new Promise<void>((resolve) => setImmediate(resolve));
730
+ if (!done) t.mock.timers.tick(1_600);
731
+ }
732
+ if (!done) throw new Error("install did not settle under mocked publication timers");
733
+ if (failure) throw failure.error;
734
+ return value as T;
735
+ }
736
+
737
+ test("Windows source publication retries a transient staging lock then reuses the published bundle", async () => {
738
+ for (const [arch, goArchitecture] of [["x64", "amd64"], ["arm64", "arm64"]] as const) {
739
+ const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-windows-retry-${arch}-`));
740
+ const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: goArchitecture });
741
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
742
+ let stagingAttempts = 0;
743
+ const result = await installGentleAi({
744
+ packageRoot,
745
+ platform: "win32",
746
+ arch,
747
+ execFile: fixture.run,
748
+ resolveGoExecutable: fixture.resolveGoExecutable,
749
+ rename: async (from: string, to: string) => {
750
+ if (from.includes(".staging-") && to === versionDirectory) {
751
+ stagingAttempts += 1;
752
+ if (stagingAttempts === 1) throw codedError("EPERM", "simulated staging lock");
753
+ }
754
+ await rename(from, to);
755
+ },
756
+ });
757
+ assert.equal(result.installed, true);
758
+ assert.equal(stagingAttempts, 2);
759
+ assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1);
760
+ assert.equal(await readFile(join(versionDirectory, "gentle-ai.exe"), "utf8"), "trusted Windows source build");
761
+ assert.equal(await readFile(join(versionDirectory, "integrity.json"), "utf8"), `${JSON.stringify(expectedWindowsSourceManifest(arch))}\n`);
762
+ assert.deepEqual(await publicationLeftovers(packageRoot), []);
763
+ const reuse = await hardenedWindowsGoFixture(packageRoot, { architecture: goArchitecture });
764
+ assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch, execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
765
+ assert.equal(reuse.calls.some((call) => call.arguments_[0] === "install"), false);
766
+ }
767
+ });
768
+
769
+ test("Windows publication retries EPERM, EBUSY, and EACCES through five attempts with increasing delays", async (t) => {
770
+ t.mock.timers.enable({ apis: ["setTimeout"] });
771
+ const delays = [200, 400, 800, 1600];
772
+ for (const code of ["EPERM", "EBUSY", "EACCES"] as const) {
773
+ const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-windows-retry-budget-${code}-`));
774
+ const fixture = await hardenedWindowsGoFixture(packageRoot);
775
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
776
+ let stagingAttempts = 0;
777
+ const install = installGentleAi({
778
+ packageRoot,
779
+ platform: "win32",
780
+ arch: "x64",
781
+ execFile: fixture.run,
782
+ resolveGoExecutable: fixture.resolveGoExecutable,
783
+ rename: async (from: string, to: string) => {
784
+ if (from.includes(".staging-") && to === versionDirectory) {
785
+ stagingAttempts += 1;
786
+ if (stagingAttempts < 5) throw codedError(code, `simulated ${code} lock`);
787
+ }
788
+ await rename(from, to);
789
+ },
790
+ });
791
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
792
+ await waitForCount(() => stagingAttempts, attempt, `${code} attempt ${attempt}`);
793
+ t.mock.timers.tick(delays[attempt - 1] - 1);
794
+ await new Promise<void>((resolve) => setImmediate(resolve));
795
+ assert.equal(stagingAttempts, attempt, `${code} must not retry before ${delays[attempt - 1]}ms`);
796
+ t.mock.timers.tick(1);
797
+ }
798
+ await waitForCount(() => stagingAttempts, 5, `${code} fifth attempt`);
799
+ assert.equal((await install).installed, true, `${code} must succeed on the fifth attempt without a further delay`);
800
+ assert.equal(stagingAttempts, 5);
801
+ assert.equal(fixture.calls.filter((call) => call.file === fixture.goPath && call.arguments_[0] === "install").length, 1);
802
+ }
803
+ });
804
+
805
+ test("Windows source replacement retries transient backup and staging locks then removes the backup", async () => {
806
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-replace-retry-"));
807
+ const initial = await hardenedWindowsGoFixture(packageRoot);
808
+ await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
809
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
810
+ await writeFile(join(versionDirectory, "integrity.json"), "{}\n");
811
+ const fixture = await hardenedWindowsGoFixture(packageRoot);
812
+ let backupAttempts = 0;
813
+ let stagingAttempts = 0;
814
+ const result = await installGentleAi({
815
+ packageRoot,
816
+ platform: "win32",
817
+ arch: "x64",
818
+ execFile: fixture.run,
819
+ resolveGoExecutable: fixture.resolveGoExecutable,
820
+ rename: async (from: string, to: string) => {
821
+ if (from === versionDirectory && to.includes(".backup-")) {
822
+ backupAttempts += 1;
823
+ if (backupAttempts === 1) throw codedError("EPERM", "simulated backup lock");
824
+ }
825
+ if (from.includes(".staging-") && to === versionDirectory) {
826
+ stagingAttempts += 1;
827
+ if (stagingAttempts === 1) throw codedError("EBUSY", "simulated staging lock");
828
+ }
829
+ await rename(from, to);
830
+ },
831
+ });
832
+ assert.equal(result.installed, true);
833
+ assert.equal(backupAttempts, 2);
834
+ assert.equal(stagingAttempts, 2);
835
+ assert.equal(await readFile(join(versionDirectory, "gentle-ai.exe"), "utf8"), "trusted Windows source build");
836
+ assert.equal(await readFile(join(versionDirectory, "integrity.json"), "utf8"), `${JSON.stringify(expectedWindowsSourceManifest("x64"))}\n`);
837
+ assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false);
838
+ });
839
+
840
+ test("Windows source publication restores the prior bundle after exhausting a staging lock, retrying a transient rollback", async (t) => {
841
+ t.mock.timers.enable({ apis: ["setTimeout"] });
842
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-permanent-lock-"));
843
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
844
+ await mkdir(versionDirectory, { recursive: true });
845
+ await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
846
+ const fixture = await hardenedWindowsGoFixture(packageRoot);
847
+ const stagingErrors: Error[] = [];
848
+ let rollbackAttempts = 0;
849
+ await assert.rejects(
850
+ settleWithMockedTimers(t, installGentleAi({
851
+ packageRoot,
852
+ platform: "win32",
853
+ arch: "x64",
854
+ execFile: fixture.run,
855
+ resolveGoExecutable: fixture.resolveGoExecutable,
856
+ rename: async (from: string, to: string) => {
857
+ if (from.includes(".staging-") && to === versionDirectory) {
858
+ const error = codedError("EPERM", `simulated staging lock ${stagingErrors.length + 1}`);
859
+ stagingErrors.push(error);
860
+ throw error;
861
+ }
862
+ if (from.includes(".backup-") && to === versionDirectory) {
863
+ rollbackAttempts += 1;
864
+ if (rollbackAttempts === 1) throw codedError("EBUSY", "simulated rollback lock");
865
+ }
866
+ await rename(from, to);
867
+ },
868
+ })),
869
+ (error: unknown) => error === stagingErrors[4],
870
+ );
871
+ assert.equal(stagingErrors.length, 5);
872
+ assert.equal(rollbackAttempts, 2);
873
+ assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle");
874
+ assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false);
875
+ assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false);
876
+ });
877
+
878
+ test("Windows source publication leaves the live bundle untouched when the backup move is exhausted", async (t) => {
879
+ t.mock.timers.enable({ apis: ["setTimeout"] });
880
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-backup-exhausted-"));
881
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
882
+ await mkdir(versionDirectory, { recursive: true });
883
+ await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
884
+ const fixture = await hardenedWindowsGoFixture(packageRoot);
885
+ const backupErrors: Error[] = [];
886
+ await assert.rejects(
887
+ settleWithMockedTimers(t, installGentleAi({
888
+ packageRoot,
889
+ platform: "win32",
890
+ arch: "x64",
891
+ execFile: fixture.run,
892
+ resolveGoExecutable: fixture.resolveGoExecutable,
893
+ rename: async (from: string, to: string) => {
894
+ if (from === versionDirectory && to.includes(".backup-")) {
895
+ const error = codedError("EPERM", `simulated backup lock ${backupErrors.length + 1}`);
896
+ backupErrors.push(error);
897
+ throw error;
898
+ }
899
+ await rename(from, to);
900
+ },
901
+ })),
902
+ (error: unknown) => error === backupErrors[4],
903
+ );
904
+ assert.equal(backupErrors.length, 5);
905
+ assert.equal(await readFile(join(versionDirectory, "old.txt"), "utf8"), "previous bundle");
906
+ assert.equal((await readdir(join(packageRoot, ".gentle-ai"))).some((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`)), false);
907
+ });
908
+
909
+ test("Windows source publication retains the backup when rollback retries are exhausted", async (t) => {
910
+ t.mock.timers.enable({ apis: ["setTimeout"] });
911
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-rollback-exhausted-"));
912
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
913
+ await mkdir(versionDirectory, { recursive: true });
914
+ await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
915
+ const fixture = await hardenedWindowsGoFixture(packageRoot);
916
+ const rollbackErrors: Error[] = [];
917
+ await assert.rejects(
918
+ settleWithMockedTimers(t, installGentleAi({
919
+ packageRoot,
920
+ platform: "win32",
921
+ arch: "x64",
922
+ execFile: fixture.run,
923
+ resolveGoExecutable: fixture.resolveGoExecutable,
924
+ rename: async (from: string, to: string) => {
925
+ if (from.includes(".staging-") && to === versionDirectory) throw codedError("EPERM", "simulated staging lock");
926
+ if (from.includes(".backup-") && to === versionDirectory) {
927
+ const error = codedError("EACCES", `simulated rollback lock ${rollbackErrors.length + 1}`);
928
+ rollbackErrors.push(error);
929
+ throw error;
930
+ }
931
+ await rename(from, to);
932
+ },
933
+ })),
934
+ (error: unknown) => error instanceof Error
935
+ && error.message === "Gentle AI bundle publication failed and rollback could not restore the prior bundle"
936
+ && error.cause === rollbackErrors[4],
937
+ );
938
+ assert.equal(rollbackErrors.length, 5);
939
+ assert.equal(existsSync(versionDirectory), false);
940
+ const backups = (await readdir(join(packageRoot, ".gentle-ai"))).filter((entry) => entry.startsWith(`.v${INSTALLER_VERSION}.backup-`));
941
+ assert.equal(backups.length, 1);
942
+ assert.equal(await readFile(join(packageRoot, ".gentle-ai", backups[0], "old.txt"), "utf8"), "previous bundle");
943
+ });
944
+
945
+ test("Windows source restores a valid backup through a transient lock on the next install", async () => {
946
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-restore-retry-"));
947
+ const initial = await hardenedWindowsGoFixture(packageRoot);
948
+ await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
949
+ const live = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
950
+ const backup = backupBundlePath(packageRoot, "restore");
951
+ await rename(live, backup);
952
+ const recovery = await hardenedWindowsGoFixture(packageRoot);
953
+ let restoreAttempts = 0;
954
+ let buildAttempted = false;
955
+ const run = async (...arguments_: Parameters<typeof recovery.run>) => {
956
+ if (arguments_[1][0] === "install") { buildAttempted = true; throw new Error("recovery must precede a new build"); }
957
+ return recovery.run(...arguments_);
958
+ };
959
+ const restored = await installGentleAi({
960
+ packageRoot,
961
+ platform: "win32",
962
+ arch: "x64",
963
+ execFile: run,
964
+ resolveGoExecutable: recovery.resolveGoExecutable,
965
+ rename: async (from: string, to: string) => {
966
+ if (from.includes(".backup-") && to === live) {
967
+ restoreAttempts += 1;
968
+ if (restoreAttempts === 1) throw codedError("EPERM", "simulated restore lock");
969
+ }
970
+ await rename(from, to);
971
+ },
972
+ });
973
+ assert.equal(restored.installed, false);
974
+ assert.equal(restoreAttempts, 2);
975
+ assert.equal(existsSync(live), true);
976
+ assert.equal(existsSync(backup), false);
977
+ assert.equal(buildAttempted, false);
978
+ assert.equal(await readFile(join(live, "gentle-ai.exe"), "utf8"), "trusted Windows source build");
979
+ });
980
+
981
+ test("Windows publication does not retry ENOSPC or uncoded rename failures", async () => {
982
+ for (const failure of [codedError("ENOSPC", "simulated disk full"), new Error("simulated uncoded failure")]) {
983
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-nonretryable-"));
984
+ const fixture = await hardenedWindowsGoFixture(packageRoot);
985
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
986
+ let stagingAttempts = 0;
987
+ await assert.rejects(
988
+ () => installGentleAi({
989
+ packageRoot,
990
+ platform: "win32",
991
+ arch: "x64",
992
+ execFile: fixture.run,
993
+ resolveGoExecutable: fixture.resolveGoExecutable,
994
+ rename: async (from: string, to: string) => {
995
+ if (from.includes(".staging-") && to === versionDirectory) {
996
+ stagingAttempts += 1;
997
+ throw failure;
998
+ }
999
+ await rename(from, to);
1000
+ },
1001
+ }),
1002
+ (error: unknown) => error === failure,
1003
+ );
1004
+ assert.equal(stagingAttempts, 1);
1005
+ assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false);
1006
+ }
1007
+ });
1008
+
1009
+ test("Darwin/Linux signed publication fails on EPERM after one attempt", async () => {
1010
+ const payload = Buffer.from("signed archive fixture");
1011
+ const binary = "signed binary";
1012
+ for (const [platform, key] of [["darwin", "darwin/amd64"], ["linux", "linux/amd64"]] as const) {
1013
+ const packageRoot = await mkdtemp(join(tmpdir(), `gentle-pi-installer-signed-noretry-${platform}-`));
1014
+ const asset = {
1015
+ name: `gentle-ai_${INSTALLER_VERSION}_${platform}_amd64.tar.gz`,
1016
+ sha256: createHash("sha256").update(payload).digest("hex"),
1017
+ binarySha256: createHash("sha256").update(binary).digest("hex"),
1018
+ url: "https://example.invalid/gentle-ai.tar.gz",
1019
+ executable: "gentle-ai",
1020
+ };
1021
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
1022
+ let stagingAttempts = 0;
1023
+ await assert.rejects(
1024
+ () => installGentleAi({
1025
+ packageRoot,
1026
+ platform,
1027
+ arch: "x64",
1028
+ releaseAssets: { [key]: asset },
1029
+ download: async (_url: string, destination: string) => writeFile(destination, payload),
1030
+ extractArchive: async (_archive: string, destination: string) => {
1031
+ await mkdir(destination, { recursive: true });
1032
+ await writeFile(join(destination, "gentle-ai"), binary);
1033
+ },
1034
+ rename: async (from: string, to: string) => {
1035
+ if (from.includes(".staging-") && to === versionDirectory) {
1036
+ stagingAttempts += 1;
1037
+ throw codedError("EPERM", "simulated staging lock");
1038
+ }
1039
+ await rename(from, to);
1040
+ },
1041
+ }),
1042
+ (error: unknown) => error instanceof Error && (error as NodeJS.ErrnoException).code === "EPERM",
1043
+ );
1044
+ assert.equal(stagingAttempts, 1, platform);
1045
+ assert.equal(existsSync(join(versionDirectory, "gentle-ai")), false);
1046
+ }
1047
+ });
1048
+
1049
+ test("Windows source install with a persistent publication lock cleans staging without pretending a runtime exists", async (t) => {
1050
+ t.mock.timers.enable({ apis: ["setTimeout"] });
1051
+ const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-windows-persistent-lock-"));
1052
+ const fixture = await hardenedWindowsGoFixture(packageRoot);
1053
+ const versionDirectory = join(packageRoot, ".gentle-ai", `v${INSTALLER_VERSION}`);
1054
+ const stagingErrors: Error[] = [];
1055
+ await assert.rejects(
1056
+ settleWithMockedTimers(t, installGentleAi({
1057
+ packageRoot,
1058
+ platform: "win32",
1059
+ arch: "x64",
1060
+ execFile: fixture.run,
1061
+ resolveGoExecutable: fixture.resolveGoExecutable,
1062
+ rename: async (from: string, to: string) => {
1063
+ if (from.includes(".staging-") && to === versionDirectory) {
1064
+ const error = codedError("EPERM", `simulated staging lock ${stagingErrors.length + 1}`);
1065
+ stagingErrors.push(error);
1066
+ throw error;
1067
+ }
1068
+ await rename(from, to);
1069
+ },
1070
+ })),
1071
+ (error: unknown) => error === stagingErrors[4],
1072
+ );
1073
+ assert.equal(stagingErrors.length, 5);
1074
+ assert.equal(existsSync(join(versionDirectory, "gentle-ai.exe")), false);
1075
+ assert.equal(existsSync(versionDirectory), false);
1076
+ assert.deepEqual(await publicationLeftovers(packageRoot), []);
1077
+ });
1078
+
615
1079
  test("Darwin/Linux signed bundles retain their four-field manifest and reusable compatibility", async () => {
616
1080
  const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-signed-compatibility-"));
617
1081
  const payload = Buffer.from("signed archive fixture");
@@ -628,7 +1092,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
628
1092
  },
629
1093
  };
630
1094
  await installGentleAi(options);
631
- const manifestPath = join(packageRoot, ".gentle-ai", "v4.0.0", "integrity.json");
1095
+ const manifestPath = join(packageRoot, ".gentle-ai", "v4.1.1", "integrity.json");
632
1096
  const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
633
1097
  assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
634
1098
  assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
@@ -641,7 +1105,7 @@ test("a pinned asset falls back to its gentle-pi mirror only on download failure
641
1105
  // under the current stable pin, whose form gate refuses raw binaries.
642
1106
  const payload = Buffer.from("signed archive fixture");
643
1107
  const binary = "mirrored binary";
644
- const baseAsset = { name: "gentle-ai_4.0.0_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/upstream", mirrorUrl: "https://example.invalid/mirror", executable: "gentle-ai" };
1108
+ const baseAsset = { name: "gentle-ai_4.1.1_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/upstream", mirrorUrl: "https://example.invalid/mirror", executable: "gentle-ai" };
645
1109
  const extractArchive = async (_archive: string, destination: string) => {
646
1110
  await mkdir(destination, { recursive: true });
647
1111
  await writeFile(join(destination, "gentle-ai"), binary);
@@ -724,21 +1188,47 @@ function pendingRequest() {
724
1188
  pending.setTimeout = () => undefined;
725
1189
  return pending;
726
1190
  }
727
- test("download bounds stalled headers and bodies with transient retry exhaustion", async () => {
1191
+ test("download bounds stalled headers and bodies with transient retry exhaustion", async (t) => {
728
1192
  for (const [stage, request] of [
729
- ["headers", () => pendingRequest()],
1193
+ ["headers", (_url: URL, _options: unknown, _callback: (response: PassThrough & { statusCode?: number; headers: Record<string, string> }) => void) => pendingRequest()],
730
1194
  ["body", (_url: URL, _options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record<string, string> }) => void) => {
731
1195
  const response = Object.assign(new PassThrough(), { statusCode: 200, headers: {} });
732
1196
  queueMicrotask(() => callback(response));
733
1197
  return pendingRequest();
734
1198
  }],
735
1199
  ] as const) {
1200
+ // A private directory per run: the PID-derived path this test used before lived
1201
+ // in the shared tmpdir and collided with leftovers from earlier runs, which is
1202
+ // how an EEXIST from the exclusive open reached the assertion instead of the
1203
+ // timeout under test.
1204
+ const directory = await mkdtemp(join(tmpdir(), "gentle-pi-stalled-"));
1205
+ t.after(() => rm(directory, { recursive: true, force: true }));
1206
+ const destination = join(directory, stage);
736
1207
  let attempts = 0;
737
- await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive", join(tmpdir(), `gentle-pi-stalled-${stage}-${process.pid}`), 1024, 0, { request: (...args: never[]) => { attempts += 1; return request(...args); }, headerTimeoutMs: 1, bodyTimeoutMs: 1, maxAttempts: 2, retryDelayMs: 0 }), new RegExp(`download ${stage} timed out`));
1208
+ await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive", destination, 1024, 0, { request: (url: URL, options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record<string, string> }) => void) => { attempts += 1; return request(url, options, callback); }, headerTimeoutMs: 1, bodyTimeoutMs: 1, maxAttempts: 2, retryDelayMs: 0 }), new RegExp(`download ${stage} timed out`));
738
1209
  assert.equal(attempts, 2);
1210
+ // A failed attempt settles only once its destination stream closed, so the retry
1211
+ // never races a late recreation of the path, and nothing partial survives.
1212
+ assert.equal(existsSync(destination), false, `a failed ${stage} download leaves no partial file behind`);
739
1213
  }
740
1214
  });
741
1215
 
1216
+ test("download never removes a destination it did not create", async (t) => {
1217
+ const directory = await mkdtemp(join(tmpdir(), "gentle-pi-existing-"));
1218
+ t.after(() => rm(directory, { recursive: true, force: true }));
1219
+ const destination = join(directory, "archive");
1220
+ await writeFile(destination, "a file this download never owned");
1221
+ // A single attempt on purpose: the retry path removes the destination on its own,
1222
+ // so the ownership rule is only observable without a retry.
1223
+ const request = (_url: URL, _options: unknown, callback: (response: PassThrough & { statusCode?: number; headers: Record<string, string> }) => void) => {
1224
+ const response = Object.assign(new PassThrough(), { statusCode: 200, headers: {} });
1225
+ queueMicrotask(() => callback(response));
1226
+ return pendingRequest();
1227
+ };
1228
+ await assert.rejects(() => downloadGentleAiAsset("https://example.invalid/archive", destination, 1024, 0, { request, headerTimeoutMs: 1, bodyTimeoutMs: 1, maxAttempts: 1, retryDelayMs: 0 }));
1229
+ assert.equal(await readFile(destination, "utf8"), "a file this download never owned");
1230
+ });
1231
+
742
1232
  test("download retries only transient HTTP statuses and exhausts within the attempt bound", async () => {
743
1233
  for (const [status, expectedAttempts] of [[429, 2], [500, 2], [502, 2], [503, 2], [504, 2], [400, 1], [404, 1]] as const) {
744
1234
  let attempts = 0;
@@ -759,7 +1249,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
759
1249
  }),
760
1250
  /checksum mismatch/,
761
1251
  );
762
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
1252
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
763
1253
  assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
764
1254
  });
765
1255
 
@@ -781,7 +1271,7 @@ test("installer promotes only the expected regular executable with executable PO
781
1271
  await chmod(extracted, 0o700);
782
1272
  },
783
1273
  });
784
- const binary = join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai");
1274
+ const binary = join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai");
785
1275
  assert.equal(existsSync(binary), true);
786
1276
  assert.equal(await readFile(binary, "utf8"), "native executable");
787
1277
  assert.ok(((await stat(binary)).mode & 0o111) !== 0);
@@ -812,7 +1302,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
812
1302
  }),
813
1303
  /binary checksum mismatch/,
814
1304
  );
815
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
1305
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
816
1306
  });
817
1307
 
818
1308
  test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
@@ -835,7 +1325,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
835
1325
  },
836
1326
  };
837
1327
  await installGentleAi(options);
838
- const binary = join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai");
1328
+ const binary = join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai");
839
1329
  await chmod(binary, 0o600);
840
1330
  const repaired = await installGentleAi(options);
841
1331
  assert.equal(repaired.installed, true);
@@ -872,7 +1362,7 @@ test("installer rejects archives with multiple expected executable entries", asy
872
1362
  }),
873
1363
  /exactly one regular gentle-ai/,
874
1364
  );
875
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
1365
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
876
1366
  });
877
1367
 
878
1368
  test("installer rejects an archive without the expected regular executable", async () => {
@@ -890,7 +1380,7 @@ test("installer rejects an archive without the expected regular executable", asy
890
1380
  }),
891
1381
  /non-regular gentle-ai/,
892
1382
  );
893
- assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.0.0", "gentle-ai")), false);
1383
+ assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v4.1.1", "gentle-ai")), false);
894
1384
  });
895
1385
 
896
1386
  test("safeRemoveDirectory passes Windows-safe retry options to recursive rm cleanup", async () => {