gentle-pi 3.7.0 → 4.1.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 (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  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 +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,317 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { appendSystemPromptOnce, type AppendableSystemPromptOptions } from "./append-system-prompt.ts";
3
+ import { sameReviewSessionIdentity, resolveCanonicalGitRepositoryIdentity, type ReviewSessionIdentity } from "./review-session-standing-permission.ts";
4
+ import { realpath } from "node:fs/promises";
5
+ import { gentlePiConfigHome } from "./agent-home.ts";
6
+ import { isPermissionMode, resolvePermissionModePolicy, writePermissionModePolicy, type PermissionMode } from "./permission-mode-policy.ts";
7
+ export type { PermissionMode } from "./permission-mode-policy.ts";
8
+ export const MODE_STATUS_KEY = "gentle:mode";
9
+ export const AUTO_MODE_NOTICE = "Auto uses additional model calls with separate usage accounting; hard safety and independent consent remain.";
10
+
11
+ export const YOLO_STATUS_KEY = "gentle:yolo";
12
+ export const YOLO_STATUS_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
13
+ export const YOLO_DIRECTIVE = `<gentle-yolo-session>
14
+ YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace (directly or through a trusted global default).
15
+ Qualify the default commit, push and PR confirmation clauses: for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation, this activation supplies standing permission instead of repeated permission questions. Make ordinary reversible implementation choices without needless interviews.
16
+ This does NOT override explicit human restrictions, repository policy, project trust, configured confirmations or blocks, or the authorized task scope. Ask before scope expansion, privacy-sensitive disclosure, genuinely unresolved consequential product choices, or ambiguous destinations or credentials. Never invent destinations or credentials.
17
+ Destructive/data-loss confirmations remain mandatory and independent. Never auto-answer ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Review standing permission and RDD are unchanged. Children remain bounded by their delegated task; they receive no independent delivery grant or YOLO inheritance.
18
+ </gentle-yolo-session>`;
19
+
20
+ /** Legacy standalone API; separate from review grants and their registry. */
21
+ export class YoloSessionPolicy {
22
+ private grant: ReviewSessionIdentity | undefined;
23
+ private generation = 0;
24
+ get epoch(): number { return this.generation; }
25
+ get enabled(): boolean { return this.grant !== undefined; }
26
+ reset(): void { this.generation += 1; this.grant = undefined; }
27
+ set(enabled: boolean, identity: ReviewSessionIdentity | undefined, expectedEpoch = this.epoch): boolean {
28
+ if (expectedEpoch !== this.epoch) return false;
29
+ this.reset();
30
+ if (!enabled) return true;
31
+ if (identity === undefined) return false;
32
+ this.grant = identity;
33
+ return true;
34
+ }
35
+ active(identity: ReviewSessionIdentity | undefined): boolean {
36
+ if (!this.grant) return false;
37
+ if (identity !== undefined && sameReviewSessionIdentity(this.grant, identity)) return true;
38
+ this.reset();
39
+ return false;
40
+ }
41
+ }
42
+
43
+ /** Remove our exact block as well as adding it: bridge options can be reused. */
44
+ export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean): void {
45
+ if (!options) return;
46
+ options.appendSystemPrompt = (options.appendSystemPrompt ?? "")
47
+ .replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "");
48
+ if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
49
+ }
50
+ const HUMAN_ACTION = { on: "on", off: "off", toggle: "" } as const;
51
+ type HumanAction = (typeof HUMAN_ACTION)[keyof typeof HUMAN_ACTION];
52
+ export const YOLO_DISPLAY = { on: "ON", off: "OFF", unavailable: "UNAVAILABLE" } as const;
53
+ export type YoloDisplay = (typeof YOLO_DISPLAY)[keyof typeof YOLO_DISPLAY];
54
+ const HOST_UI_CHANNEL = "gentle:yolo:host-ui";
55
+ const DISCOVERY_TIMEOUT_MS = 100;
56
+ export interface YoloUiAdapter {
57
+ read(): Promise<YoloDisplay>;
58
+ readMode?(): Promise<PermissionMode>;
59
+ readSnapshot?(): Promise<{ yolo: YoloDisplay; mode: PermissionMode; defaultMode: PermissionMode }>;
60
+ setMode?(mode: PermissionMode): Promise<boolean>;
61
+ readDefault?(): PermissionMode;
62
+ saveDefault?(mode: PermissionMode): Promise<boolean>;
63
+ toggle(): Promise<void>;
64
+ observe(refresh: () => void): () => void;
65
+ dispose(): void;
66
+ }
67
+ interface HostUiRequest { context: ExtensionContext; respond(adapter: YoloUiAdapter): void }
68
+ function isUiAdapter(value: unknown): value is YoloUiAdapter {
69
+ if (!value || typeof value !== "object") return false;
70
+ const adapter = value as Partial<YoloUiAdapter>;
71
+ return [adapter.read, adapter.toggle, adapter.observe, adapter.dispose].every(fn => typeof fn === "function");
72
+ }
73
+ /** emit is void (and listeners may be wrapped async); explicitly bound the callback wait. */
74
+ export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContext): Promise<YoloUiAdapter | undefined> {
75
+ return new Promise(resolve => {
76
+ let settled = false;
77
+ const timer = setTimeout(() => { settled = true; resolve(undefined); }, DISCOVERY_TIMEOUT_MS);
78
+ const respond = (value: unknown) => {
79
+ if (!isUiAdapter(value)) return;
80
+ if (settled) { value.dispose(); return; }
81
+ settled = true; clearTimeout(timer); resolve(value);
82
+ };
83
+ try { pi.events.emit(HOST_UI_CHANNEL, { context, respond } satisfies HostUiRequest); }
84
+ catch { settled = true; clearTimeout(timer); resolve(undefined); }
85
+ });
86
+ }
87
+ export interface YoloSessionController {
88
+ active(context: ExtensionContext): Promise<boolean>;
89
+ mode(context: ExtensionContext): Promise<PermissionMode>;
90
+ setMode(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
91
+ defaultMode(): PermissionMode;
92
+ saveDefault(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
93
+ epoch(): number;
94
+ humanAction(action: HumanAction, context: ExtensionContext, current?: () => boolean): Promise<boolean>;
95
+ reset(context: ExtensionContext): void;
96
+ }
97
+ interface ModeIdentity {
98
+ manager: ExtensionContext["sessionManager"];
99
+ id: string;
100
+ workspace: string;
101
+ }
102
+ /** UI/slash authoring only. Defaults are read from the dedicated trusted user store. */
103
+ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessEnv,
104
+ options: { onChange?: () => void } = {}): YoloSessionController {
105
+ const store = { gentlePiConfigHome: gentlePiConfigHome(env) };
106
+ let generation = 0;
107
+ let ownerGeneration = 0;
108
+ let liveMode: PermissionMode = "normal";
109
+ let binding: ModeIdentity | undefined;
110
+ let interactiveGrant = false;
111
+ // A same-session revocation must not replay the global elevated preference.
112
+ const seen = new WeakMap<object, Set<string>>();
113
+ const observers = new Set<() => void>();
114
+ const pendingCaptures = new Set<{ manager: ModeIdentity["manager"]; id: string }>();
115
+ const primary = () => env.GENTLE_PI_AGENTS_CHILD !== "1";
116
+ const interactive = (ctx: ExtensionContext): boolean => {
117
+ if (!primary() || !ctx.hasUI) return false;
118
+ const mode = (ctx as ExtensionContext & { mode?: string }).mode;
119
+ if (mode !== undefined) return mode === "tui";
120
+ try { return (ctx.ui.getAllThemes?.().length ?? 0) > 0; } catch { return false; }
121
+ };
122
+ const session = (ctx: ExtensionContext): { manager: ModeIdentity["manager"]; id: string } | undefined => {
123
+ try {
124
+ const manager = ctx.sessionManager, id = manager.getSessionId();
125
+ return manager && typeof id === "string" && id.length > 0 ? { manager, id } : undefined;
126
+ } catch { return undefined; }
127
+ };
128
+ const remember = (scope: { manager: object; id: string }): void => {
129
+ const ids = seen.get(scope.manager) ?? new Set<string>(); ids.add(scope.id); seen.set(scope.manager, ids);
130
+ };
131
+ const publish = (ctx: ExtensionContext): void => {
132
+ const yolo = liveMode === "yolo";
133
+ try { ctx.ui.setStatus(YOLO_STATUS_KEY, yolo ? YOLO_STATUS_TEXT : undefined); } catch { /* display only */ }
134
+ try { ctx.ui.setWidget(YOLO_STATUS_KEY, yolo ? [YOLO_STATUS_TEXT] : undefined); } catch { /* display only */ }
135
+ try { ctx.ui.setStatus(MODE_STATUS_KEY, `Permissions: ${liveMode.toUpperCase()}`); } catch { /* display only */ }
136
+ };
137
+ const changed = (ctx: ExtensionContext): void => {
138
+ generation += 1; publish(ctx);
139
+ try { options.onChange?.(); } catch { /* observer is not authority */ }
140
+ for (const refresh of [...observers]) { try { refresh(); } catch { /* display only */ } }
141
+ };
142
+ const notify = (ctx: ExtensionContext, message: string, warning = false): void => {
143
+ try { ctx.ui.notify(message, warning ? "warning" : "info"); } catch { /* headless/stale UI */ }
144
+ };
145
+ // Capture plain identity before async filesystem work; recheck after every await.
146
+ const bindCurrent = (ctx: ExtensionContext, disposed = () => false, requireUI = true): (() => boolean) => {
147
+ const owner = ownerGeneration, scope = session(ctx), cwd = ctx.cwd;
148
+ return () => {
149
+ try {
150
+ const now = session(ctx);
151
+ return !disposed() && owner === ownerGeneration && !!scope && !!now &&
152
+ scope.manager === now.manager && scope.id === now.id && cwd === ctx.cwd &&
153
+ (!requireUI || interactive(ctx));
154
+ } catch { return false; }
155
+ };
156
+ };
157
+ const capture = async (ctx: ExtensionContext): Promise<ModeIdentity | undefined> => {
158
+ if (!primary()) return undefined;
159
+ const scope = session(ctx), cwd = ctx.cwd, current = bindCurrent(ctx, () => false, false);
160
+ if (!scope) return undefined;
161
+ pendingCaptures.add(scope);
162
+ try {
163
+ const canonical = await realpath(cwd);
164
+ if (!current()) return undefined;
165
+ const clone = await resolveCanonicalGitRepositoryIdentity(canonical);
166
+ if (!current()) return undefined;
167
+ return { ...scope, workspace: clone ?? `cwd:${canonical}` };
168
+ } catch { return undefined; }
169
+ finally { pendingCaptures.delete(scope); }
170
+ };
171
+ const defaultMode = (): PermissionMode => resolvePermissionModePolicy(store).mode;
172
+ const mode = async (ctx: ExtensionContext): Promise<PermissionMode> => {
173
+ const epoch = generation;
174
+ const identity = await capture(ctx);
175
+ if (epoch !== generation) return "normal";
176
+ if (binding && (!identity || binding.manager !== identity.manager || binding.id !== identity.id ||
177
+ binding.workspace !== identity.workspace || (interactiveGrant && !interactive(ctx)))) {
178
+ binding = undefined; liveMode = "normal"; interactiveGrant = false; changed(ctx);
179
+ }
180
+ if (!binding && identity) {
181
+ const fresh = !seen.get(identity.manager)?.has(identity.id);
182
+ remember(identity); binding = identity;
183
+ if (fresh) { liveMode = defaultMode(); interactiveGrant = false; changed(ctx); }
184
+ }
185
+ publish(ctx);
186
+ return identity && binding ? liveMode : "normal";
187
+ };
188
+ const active = async (ctx: ExtensionContext): Promise<boolean> => await mode(ctx) === "yolo";
189
+ const reset = (ctx: ExtensionContext): void => {
190
+ ownerGeneration += 1;
191
+ // Initial session_start remains fresh; shutdown/reload of an observed session revokes.
192
+ if (binding) remember(binding);
193
+ const scope = session(ctx);
194
+ if (scope && [...pendingCaptures].some(pending => pending.manager === scope.manager && pending.id === scope.id)) remember(scope);
195
+ binding = undefined; liveMode = "normal"; interactiveGrant = false;
196
+ changed(ctx); observers.clear();
197
+ };
198
+ const setMode = async (requested: PermissionMode, ctx: ExtensionContext, lifetime = () => true): Promise<boolean> => {
199
+ if (!isPermissionMode(requested) || !interactive(ctx) || !lifetime()) return false;
200
+ const current = bindCurrent(ctx, () => !lifetime());
201
+ // Revoke immediately, including off versus a pending activation race.
202
+ const scope = session(ctx); if (scope) remember(scope);
203
+ binding = undefined; liveMode = "normal"; interactiveGrant = false; changed(ctx);
204
+ const epoch = generation;
205
+ const identity = await capture(ctx);
206
+ if (!current() || epoch !== generation || !identity) return false;
207
+ binding = identity; remember(identity); liveMode = requested; interactiveGrant = true; changed(ctx);
208
+ if (requested === "auto") notify(ctx, AUTO_MODE_NOTICE);
209
+ return true;
210
+ };
211
+ const saveDefault = async (requested: PermissionMode, ctx: ExtensionContext, lifetime = () => true): Promise<boolean> => {
212
+ if (!isPermissionMode(requested) || !interactive(ctx) || !lifetime()) return false;
213
+ const current = bindCurrent(ctx, () => !lifetime()), epoch = generation;
214
+ const identity = await capture(ctx);
215
+ if (!identity || !current() || epoch !== generation) return false;
216
+ // Initialize before saving, without another await that could let a superseding
217
+ // mode action authorize this stale save. Observer reentrancy is checked too.
218
+ if (!binding || binding.manager !== identity.manager || binding.id !== identity.id || binding.workspace !== identity.workspace) {
219
+ liveMode = seen.get(identity.manager)?.has(identity.id) ? "normal" : defaultMode();
220
+ binding = identity; remember(identity); interactiveGrant = false; changed(ctx);
221
+ if (!current() || generation !== epoch + 1) return false;
222
+ }
223
+ try { writePermissionModePolicy(requested, store); }
224
+ catch { notify(ctx, "Default unchanged — permission mode preference is malformed, unreadable or could not be saved.", true); return false; }
225
+ notify(ctx, `Default ${requested.toUpperCase()} saved for next primary sessions.${requested === "auto" ? ` ${AUTO_MODE_NOTICE}` : ""}`);
226
+ for (const refresh of [...observers]) { try { refresh(); } catch { /* display only */ } }
227
+ return true;
228
+ };
229
+ const humanAction = async (action: HumanAction, ctx: ExtensionContext, current = () => true): Promise<boolean> => {
230
+ if (!current()) return false;
231
+ const enable = action === HUMAN_ACTION.on || (action === HUMAN_ACTION.toggle && liveMode !== "yolo");
232
+ const pending = setMode(enable ? "yolo" : "normal", ctx, current);
233
+ const epoch = generation; // setMode revokes synchronously, then publishes once on success.
234
+ const result = await pending;
235
+ if (!current()) return false;
236
+ // Supersession is not activation failure: the winning action owns notification.
237
+ if (generation !== epoch + (result ? 1 : 0)) return true;
238
+ notify(ctx, result ? enable ? YOLO_STATUS_TEXT : "YOLO OFF" :
239
+ "YOLO OFF — activation requires an interactive primary TUI session and an identifiable workspace.", !result);
240
+ return true;
241
+ };
242
+ pi.events?.on?.(HOST_UI_CHANNEL, (payload: unknown) => {
243
+ if (!payload || typeof payload !== "object") return;
244
+ const request = payload as Partial<HostUiRequest>;
245
+ if (typeof request.respond !== "function" || !request.context) return;
246
+ const ctx = request.context;
247
+ try {
248
+ let disposed = false;
249
+ const subscriptions = new Set<() => void>();
250
+ const current = bindCurrent(ctx, () => disposed);
251
+ const read = async (): Promise<YoloDisplay> => {
252
+ if (!current()) return YOLO_DISPLAY.unavailable;
253
+ const epoch = generation, value = await mode(ctx);
254
+ if (!current()) return YOLO_DISPLAY.unavailable;
255
+ if (epoch !== generation) return read();
256
+ return binding ? value === "yolo" ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
257
+ };
258
+ request.respond({
259
+ read,
260
+ readSnapshot: async () => {
261
+ const yolo = await read();
262
+ return { yolo, mode: current() ? liveMode : "normal", defaultMode: defaultMode() };
263
+ },
264
+ readMode: async () => current() ? mode(ctx) : "normal",
265
+ setMode: async value => setMode(value, ctx, current),
266
+ readDefault: defaultMode,
267
+ saveDefault: async value => saveDefault(value, ctx, current),
268
+ toggle: async () => { await humanAction(HUMAN_ACTION.toggle, ctx, current); },
269
+ observe: refresh => {
270
+ if (!current()) return () => {};
271
+ subscriptions.add(refresh); observers.add(refresh);
272
+ return () => { subscriptions.delete(refresh); observers.delete(refresh); };
273
+ },
274
+ dispose: () => { disposed = true; for (const refresh of subscriptions) observers.delete(refresh); subscriptions.clear(); },
275
+ });
276
+ } catch { /* obsolete trusted context: discovery expires closed */ }
277
+ });
278
+ pi.registerCommand("gentle:mode", {
279
+ description: "Permissions: normal|auto|yolo|status; default normal|auto|yolo saves for new primary sessions.",
280
+ handler: async (args, ctx) => {
281
+ const words = args.trim().split(/\s+/);
282
+ if (!args.trim() || args.trim() === "status") {
283
+ notify(ctx, `Permissions: ${(await mode(ctx)).toUpperCase()} — default: ${defaultMode().toUpperCase()}`); return;
284
+ }
285
+ if (words.length === 2 && words[0] === "default" && isPermissionMode(words[1])) {
286
+ await saveDefault(words[1], ctx); return;
287
+ }
288
+ if (words.length === 1 && isPermissionMode(words[0])) {
289
+ if (await setMode(words[0], ctx)) notify(ctx, `Permissions: ${words[0].toUpperCase()}`);
290
+ else notify(ctx, "Mode unchanged — interactive primary session required or the session changed.", true);
291
+ return;
292
+ }
293
+ notify(ctx, "Use /gentle:mode normal|auto|yolo|status or /gentle:mode default normal|auto|yolo. State unchanged.", true);
294
+ },
295
+ });
296
+ pi.registerCommand("gentle:yolo", {
297
+ description: "Ordinary development/delivery permission (enable|disable|status); no argument opens a menu. Destructive confirmations remain.",
298
+ handler: async (args, ctx) => {
299
+ let action = args.trim() || "status";
300
+ let current = () => true;
301
+ if (!args.trim() && ctx.hasUI && typeof ctx.ui.select === "function") {
302
+ current = bindCurrent(ctx);
303
+ const state = await active(ctx) ? YOLO_DISPLAY.on : YOLO_DISPLAY.off;
304
+ const selected = await ctx.ui.select(`🚀 Gentle YOLO 🔥 — full speed, destructive actions still ask (current: ${state})`, ["enable", "disable", "status"]);
305
+ if (selected === undefined) return;
306
+ action = selected;
307
+ }
308
+ if (action !== "enable" && action !== "disable" && action !== "status") {
309
+ notify(ctx, "Use /gentle:yolo enable|disable|status. State unchanged.", true); return;
310
+ }
311
+ if (action === "status") { notify(ctx, await active(ctx) ? YOLO_STATUS_TEXT : "YOLO OFF"); return; }
312
+ if (await humanAction(action === "enable" ? HUMAN_ACTION.on : HUMAN_ACTION.off, ctx, current)) return;
313
+ notify(ctx, "YOLO unchanged — the session changed while the menu was open.", true);
314
+ },
315
+ });
316
+ return { active, mode, setMode, defaultMode, saveDefault, epoch: () => generation, humanAction, reset };
317
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "3.7.0",
4
- "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
3
+ "version": "4.1.0",
4
+ "description": "Turn Pi into el Gentleman: an ODD development harness with focused subagents, configured TDD evidence, native review, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -13,8 +13,7 @@
13
13
  "pi-coding-agent",
14
14
  "gentle-pi",
15
15
  "gentle-ai",
16
- "sdd",
17
- "openspec",
16
+ "odd",
18
17
  "subagents",
19
18
  "tdd",
20
19
  "code-review",
@@ -37,6 +36,7 @@
37
36
  "skills/",
38
37
  "scripts/",
39
38
  "tests/",
39
+ "!tests/fixtures/legacy/sdd-research-v2.5.0.md",
40
40
  "themes/",
41
41
  "README.md"
42
42
  ],
@@ -44,7 +44,7 @@
44
44
  "check:provider-contract": "node scripts/check-provider-contract.mjs",
45
45
  "mirror:odd-routing": "node scripts/mirror-odd-routing.mjs",
46
46
  "postinstall": "node scripts/install-gentle-ai.mjs",
47
- "test": "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
47
+ "test": "node scripts/run-test-suite.mjs",
48
48
  "test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
49
49
  "test:dev-binary": "node --experimental-strip-types --test tests/devbinary/*.devtest.ts",
50
50
  "test:cross-lane": "node tests/crosslane/cross-lane.mjs",
@@ -72,22 +72,34 @@
72
72
  ]
73
73
  },
74
74
  "dependencies": {
75
- "@earendil-works/pi-tui": "0.85.1",
76
75
  "@heyhuynhgiabuu/pi-pretty": "0.6.27"
77
76
  },
78
77
  "peerDependencies": {
79
- "@earendil-works/pi-coding-agent": ">=0.85.1",
78
+ "@earendil-works/pi-coding-agent": ">=0.99.1",
79
+ "@earendil-works/pi-ai": "*",
80
+ "@earendil-works/pi-tui": "*",
80
81
  "typebox": "*"
81
82
  },
82
83
  "peerDependenciesMeta": {
83
84
  "@earendil-works/pi-coding-agent": {
84
85
  "optional": true
86
+ },
87
+ "@earendil-works/pi-ai": {
88
+ "optional": true
89
+ },
90
+ "@earendil-works/pi-tui": {
91
+ "optional": true
85
92
  }
86
93
  },
87
94
  "devDependencies": {
88
- "@earendil-works/pi-coding-agent": "0.85.1",
95
+ "@earendil-works/pi-coding-agent": ">=1.0.0",
96
+ "@earendil-works/pi-ai": ">=1.0.0",
97
+ "@earendil-works/pi-tui": ">=1.0.0",
89
98
  "@types/node": "^24.13.3",
90
99
  "typescript": "^5.9.3"
91
100
  },
101
+ "engines": {
102
+ "node": ">=22.19.0"
103
+ },
92
104
  "packageManager": "pnpm@11.1.1"
93
105
  }
@@ -0,0 +1,63 @@
1
+ // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
+ import { isAbsolute } from "node:path";
3
+
4
+ // #1690: the gentle-shell launcher injects the gentle-pi package into the
5
+ // parent pi with `-e` instead of a settings declaration, so delegated children
6
+ // would otherwise start without it. The launcher records the exact extension
7
+ // set it injected in this variable; the subagent runner forwards that set to
8
+ // every child. Kept free of launcher imports so the runner can load it cheaply.
9
+ export const CHILD_PACKAGE_INJECTION_ENV = "GENTLE_SHELL_CHILD_PACKAGE_INJECTION";
10
+
11
+ const CHILD_PACKAGE_INJECTION_VERSION = 1;
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+ export function encodeChildPackageInjection(value ) {
22
+ return JSON.stringify({
23
+ version: CHILD_PACKAGE_INJECTION_VERSION,
24
+ noExtensions: value.noExtensions,
25
+ extensionPaths: value.extensionPaths,
26
+ });
27
+ }
28
+
29
+ // Returns undefined for an absent or invalid value and never throws: a bad
30
+ // signal must degrade to "no forwarding", not break a child launch. The path
31
+ // flavor is injectable so tests can check Windows paths on any platform.
32
+ export function parseChildPackageInjection(
33
+ env ,
34
+ pathFlavor = { isAbsolute },
35
+ ) {
36
+ const raw = env[CHILD_PACKAGE_INJECTION_ENV];
37
+ if (raw === undefined || raw.length === 0) return undefined;
38
+ let parsed ;
39
+ try {
40
+ parsed = JSON.parse(raw);
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
45
+ const record = parsed ;
46
+ if (record.version !== CHILD_PACKAGE_INJECTION_VERSION) return undefined;
47
+ if (typeof record.noExtensions !== "boolean") return undefined;
48
+ const paths = record.extensionPaths;
49
+ if (!Array.isArray(paths) || paths.length === 0) return undefined;
50
+ const extensionPaths = [];
51
+ for (const path of paths) {
52
+ if (typeof path !== "string" || path.length === 0 || !pathFlavor.isAbsolute(path)) return undefined;
53
+ extensionPaths.push(path);
54
+ }
55
+ return { noExtensions: record.noExtensions, extensionPaths };
56
+ }
57
+
58
+ // Plain argv elements for a child pi: spawned without a shell, so no quoting.
59
+ export function childPackageExtensionArgs(injection ) {
60
+ const args = injection.noExtensions ? ["--no-extensions"] : [];
61
+ for (const path of injection.extensionPaths) args.push("--extension", path);
62
+ return args;
63
+ }
@@ -2,7 +2,7 @@
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { dirname, isAbsolute, join, posix, relative, resolve, win32 } from "node:path";
6
6
  import {
7
7
  GENTLE_AI_INSTALL_METHOD,
8
8
  GENTLE_AI_WINDOWS_SOURCE_MODULE,
@@ -14,6 +14,8 @@ import {
14
14
  isWindowsGoSumdbSourceTarget,
15
15
  resolveGentleAiReleaseAsset,
16
16
  } from "../scripts/gentle-ai-installer.mjs";
17
+ import { engramPinVersion } from "../scripts/installer-downloads.mjs";
18
+ import { bundledGentlePiConfigHome, bundledPiConfigDir, bundledPiConfigParent, runningBundledPrefix } from "./gentle-shell-bundled.mjs";
17
19
  import { fileURLToPath } from "node:url";
18
20
 
19
21
  export const GENTLE_AI_BINARY_MISSING_CODE = "package-local-binary-missing";
@@ -46,6 +48,84 @@ export function gentleAiBinaryPath(
46
48
  );
47
49
  }
48
50
 
51
+ // ---------------------------------------------------------------------------
52
+ // Bundled isolation (S9). The bundled launcher (bin/gentle-shell.mjs) gives Pi
53
+ // and setup GENTLE_SHELL_PREFIX, its prefix. Every gentle-ai run a bundled
54
+ // gentle-pi starts gets Gentle AI's own variables (gentle-ai #5462): its state,
55
+ // backups and telemetry under <prefix>/gentle-ai, the gentle-pi config folder
56
+ // <prefix>/config/gentle-ai for its persona and background subagent files
57
+ // (and, for Gentle AI 4.0.0, GENTLE_PI_CONFIG_HOME=<prefix>/config), the pinned
58
+ // Engram in runtime/ with no Engram acquisition or dependency probe, and no
59
+ // self-update. They are never put in Pi's own environment: the agent's shell
60
+ // inherits it, and a gentle-ai the user runs there stays the user's. A
61
+ // gentle-ai release without #5462 ignores them.
62
+ // ---------------------------------------------------------------------------
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+ export function bundledGentleAiPaths(prefix , platform = process.platform) {
76
+ const path = platform === "win32" ? win32 : posix;
77
+ return {
78
+ home: path.join(prefix, "gentle-ai"),
79
+ piConfigParent: bundledPiConfigParent(prefix, platform),
80
+ piConfig: bundledPiConfigDir(prefix, platform),
81
+ engram: path.join(prefix, "runtime", `engram-${engramPinVersion}`, platform === "win32" ? "engram.exe" : "engram"),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * `env` plus GENTLE_AI_HOME, GENTLE_AI_PI_CONFIG_DIR, GENTLE_AI_ENGRAM_BIN
87
+ * (only when the pinned Engram is published), GENTLE_AI_NO_SELF_UPDATE=1 and
88
+ * GENTLE_PI_CONFIG_HOME (the parent of GENTLE_AI_PI_CONFIG_DIR)
89
+ * when this gentle-pi runs from the valid bundled install GENTLE_SHELL_PREFIX
90
+ * names (runningBundledPrefix). Otherwise `env` itself, unchanged: a gentle-pi
91
+ * installed elsewhere (the user's own, started from the agent's shell, which
92
+ * inherits the variable) keeps its own Gentle AI.
93
+ */
94
+ export function gentleAiIsolationEnv(
95
+ env ,
96
+ options = {},
97
+ ) {
98
+ const platform = options.platform ?? process.platform;
99
+ const prefix = runningBundledPrefix(env, { packageRoot: options.packageRoot, platform });
100
+ if (prefix === undefined) return env;
101
+ const paths = bundledGentleAiPaths(prefix, platform);
102
+ let engram = false;
103
+ try {
104
+ engram = lstatSync(paths.engram).isFile();
105
+ } catch {
106
+ // Not published (a prefix from before Engram shipped): Gentle AI gets none.
107
+ }
108
+ // Gentle AI 4.0.0 ignores GENTLE_AI_ENGRAM_BIN and looks `engram` up on PATH
109
+ // first, then falls back to Homebrew (`brew tap`, #2105): the pinned one goes first.
110
+ const pathKey = Object.keys(env).find((name) => name.toUpperCase() === "PATH") ?? (platform === "win32" ? "Path" : "PATH");
111
+ const pathModule = platform === "win32" ? win32 : posix;
112
+ const path = engram
113
+ ? { [pathKey]: [pathModule.dirname(paths.engram), ...(env[pathKey] ?? "").split(pathModule.delimiter).filter((entry) => entry.length > 0)].join(pathModule.delimiter) }
114
+ : {};
115
+ return {
116
+ ...env,
117
+ ...path,
118
+ GENTLE_AI_HOME: paths.home,
119
+ GENTLE_AI_PI_CONFIG_DIR: paths.piConfig,
120
+ ...(engram ? { GENTLE_AI_ENGRAM_BIN: paths.engram } : {}),
121
+ GENTLE_AI_NO_SELF_UPDATE: "1",
122
+ // Gentle AI 4.0.0 knows no GENTLE_AI_PI_CONFIG_DIR: it writes gentle-pi's
123
+ // background-subagents policy to <GENTLE_PI_CONFIG_HOME>/gentle-ai, the same
124
+ // folder. Only these Gentle AI processes get it, never Pi or its shell.
125
+ GENTLE_PI_CONFIG_HOME: paths.piConfigParent,
126
+ };
127
+ }
128
+
49
129
  function sha256(value ) {
50
130
  return createHash("sha256").update(value).digest("hex");
51
131
  }
@@ -56,7 +136,8 @@ function sha256(value ) {
56
136
  // Two explicit activation paths, in precedence order:
57
137
  // 1. GENTLE_PI_GENTLE_AI_DEV_BINARY (session override, absolute path), then
58
138
  // 2. the persistent registration file at
59
- // <GENTLE_PI_CONFIG_HOME|~/.pi/gentle-ai>/dev-binary.json with the strict
139
+ // <config home>/dev-binary.json (a bundled prefix's gentle-pi, else
140
+ // GENTLE_PI_CONFIG_HOME, else ~/.pi/gentle-ai) with the strict
60
141
  // shape {"schema":"gentle-pi.dev-binary/v1","path":"<absolute path>"}.
61
142
  //
62
143
  // The registration deliberately pins no digest: it is the unpinned field-test
@@ -114,7 +195,7 @@ function ambientDevBinaryEnvironment() {
114
195
  }
115
196
 
116
197
  export function gentleAiDevBinaryRegistrationPath(environment = ambientDevBinaryEnvironment()) {
117
- const configHome = environment.env.GENTLE_PI_CONFIG_HOME ?? join(environment.home, ".pi", "gentle-ai");
198
+ const configHome = bundledGentlePiConfigHome(environment.env) ?? environment.env.GENTLE_PI_CONFIG_HOME ?? join(environment.home, ".pi", "gentle-ai");
118
199
  return join(configHome, "dev-binary.json");
119
200
  }
120
201
 
@@ -269,6 +350,30 @@ function sameFile(before , after
269
350
  return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs;
270
351
  }
271
352
 
353
+ // Reading and hashing the whole binary on every native call is expensive, and
354
+ // on Windows antivirus scans each read. The digest is reused only while the
355
+ // file keeps its full identity; any rewrite changes ctime, which callers cannot
356
+ // set, so a replaced binary is always hashed again. Keyed per reader so an
357
+ // injected reader never sees another reader's digest.
358
+ const binaryDigestCache = new WeakMap ();
359
+
360
+ function binaryIdentity(path ) {
361
+ const details = lstatSync(path, { bigint: true });
362
+ return [details.dev, details.ino, details.size, details.mtimeNs, details.ctimeNs].join(":");
363
+ }
364
+
365
+ function binaryDigest(path , readBinary ) {
366
+ let entries = binaryDigestCache.get(readBinary);
367
+ if (entries === undefined) binaryDigestCache.set(readBinary, entries = new Map());
368
+ const identity = binaryIdentity(path);
369
+ const cached = entries.get(path);
370
+ if (cached?.identity === identity) return cached.sha256;
371
+ const digest = sha256(readBinary(path));
372
+ if (binaryIdentity(path) === identity) entries.set(path, { identity, sha256: digest });
373
+ else entries.delete(path);
374
+ return digest;
375
+ }
376
+
272
377
  export function resolveGentleAiBinary(
273
378
  packageRoot = dirname(dirname(fileURLToPath(import.meta.url))),
274
379
  platform = process.platform,
@@ -297,7 +402,7 @@ export function resolveGentleAiBinary(
297
402
  const beforeManifest = lstatSync(manifestPath);
298
403
  const manifestContents = readFileSync(manifestPath, "utf8");
299
404
  const manifest = JSON.parse(manifestContents) ;
300
- const binarySha256 = sha256(readBinary(binaryPath));
405
+ const binarySha256 = binaryDigest(binaryPath, readBinary);
301
406
  const expected = expectedRuntimeManifest(platform, binarySha256, manifest);
302
407
  if (!isCanonicalManifest(manifestContents, manifest, expected)) throw new Error("invalid runtime integrity manifest");
303
408
  const afterBinary = lstatSync(binaryPath);