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,173 @@
1
+ import {
2
+ NOTIFICATION_PRIORITY, notificationSoundFor,
3
+ type NotificationEvent, type NotificationSettings, type NotificationSound,
4
+ } from "./notification-policy.ts";
5
+
6
+ export const NOTIFICATION_TTL_MS = 2000;
7
+ export const NOTIFICATION_DEDUPE_CAPACITY = 256;
8
+ export interface NotificationOccurrence {
9
+ sessionId: string;
10
+ /** Run or task identity; sequence identifies the occurrence, not the state name. */
11
+ runId: string;
12
+ sequence: number;
13
+ event: NotificationEvent;
14
+ /** Same monotonic clock domain as SchedulerOptions.now. */
15
+ occurredAt: number;
16
+ }
17
+ export interface PlaybackPermit {
18
+ /** Backend MUST call once, synchronously immediately before spawn, after async detection/validation.
19
+ * false means do not spawn. Backend owns bounded playback timeout and must honor abort,
20
+ * settling only after its process stops. Resolving early violates serial playback. */
21
+ start(): boolean;
22
+ }
23
+ export interface SchedulerOptions {
24
+ now(): number;
25
+ setTimeout(callback: () => void, delayMs: number): number;
26
+ clearTimeout(handle: number): void;
27
+ player(sound: Exclude<NotificationSound, null>, signal: AbortSignal, permit: PlaybackPermit): Promise<void>;
28
+ /** Local diagnostics only. Exceptions here are contained; at most one report per 5 seconds. */
29
+ onError?(error: unknown): void;
30
+ dedupeCapacity?: number;
31
+ }
32
+ interface Pending {
33
+ sound: Exclude<NotificationSound, null>;
34
+ priority: number;
35
+ expiresAt: number;
36
+ readyAt: number;
37
+ /** Time spent waiting for started playback does not consume the remaining TTL. */
38
+ pausedAt?: number;
39
+ }
40
+ interface Active {
41
+ controller: AbortController;
42
+ generation: number;
43
+ started: boolean;
44
+ expiryTimer?: number;
45
+ }
46
+ /** Owner supplies validated settings and TUI/parent guards (including preview). No IO or backend detection here. */
47
+ export class NotificationScheduler {
48
+ private settings?: NotificationSettings;
49
+ private muted = false;
50
+ private sessionId?: string;
51
+ private generation = 0;
52
+ private disposed = false;
53
+ private pending?: Pending;
54
+ private active?: Active;
55
+ private timer?: number;
56
+ private lastStart = -Infinity;
57
+ private lastError = -Infinity;
58
+ private readonly seen = new Set<string>();
59
+ private readonly capacity: number;
60
+ private readonly options: SchedulerOptions;
61
+
62
+ constructor(options: SchedulerOptions) {
63
+ this.options = options;
64
+ this.capacity = options.dedupeCapacity ?? NOTIFICATION_DEDUPE_CAPACITY;
65
+ if (!Number.isInteger(this.capacity) || this.capacity < 1) throw new RangeError("Invalid dedupe capacity");
66
+ }
67
+ configure(settings: NotificationSettings, muted: boolean): void {
68
+ if (this.disposed) return;
69
+ this.settings = structuredClone(settings); this.muted = muted;
70
+ if (!settings.enabled || muted) this.invalidate();
71
+ else this.schedule();
72
+ }
73
+ resetSession(sessionId: string): void {
74
+ if (this.disposed || this.sessionId === sessionId) return;
75
+ this.invalidate(); this.sessionId = sessionId; this.seen.clear();
76
+ }
77
+ /** Returns accepted-to-coalesce, NOT a promise of playback. Off/muted occurrences are consumed too.
78
+ * Selected sound is snapshotted here; priority always comes from the event, not the sound. */
79
+ enqueue(occurrence: NotificationOccurrence): boolean {
80
+ if (this.disposed || occurrence.sessionId !== this.sessionId) return false;
81
+ const key = JSON.stringify([occurrence.sessionId, occurrence.runId, occurrence.sequence]);
82
+ if (this.seen.has(key)) return false;
83
+ this.seen.add(key);
84
+ if (this.seen.size > this.capacity) this.seen.delete(this.seen.values().next().value!);
85
+ const now = this.options.now();
86
+ const expiresAt = occurrence.occurredAt + NOTIFICATION_TTL_MS;
87
+ if (!this.automaticEnabled() || !Number.isFinite(occurrence.occurredAt) || occurrence.occurredAt > now || now >= expiresAt) return false;
88
+ const sound = notificationSoundFor(this.settings!, occurrence.event);
89
+ if (sound === null) return false;
90
+ if (this.pending && this.pending.pausedAt === undefined && now >= this.pending.expiresAt) this.pending = undefined;
91
+ const priority = NOTIFICATION_PRIORITY[occurrence.event];
92
+ if (this.pending && priority < this.pending.priority) return false;
93
+ this.pending = { sound, priority, expiresAt,
94
+ readyAt: this.pending?.readyAt ?? now + this.settings!.audio.coalesceWindowMs,
95
+ pausedAt: this.active?.started ? now : undefined };
96
+ this.schedule(); return true;
97
+ }
98
+ /** Explicit, bypasses enabled/mute only. Busy when active OR pending; never flushes autos.
99
+ * Preview starts count toward automatic minimum interval, but previews themselves are not throttled. */
100
+ preview(sound: Exclude<NotificationSound, null>): boolean {
101
+ if (this.disposed || this.active || this.pending) return false;
102
+ this.play(sound); return true;
103
+ }
104
+ dispose(): void {
105
+ if (this.disposed) return;
106
+ this.disposed = true; this.invalidate(); this.seen.clear();
107
+ }
108
+ private automaticEnabled(): boolean { return !!this.settings?.enabled && !this.muted && !this.disposed; }
109
+ private clearTimer(): void {
110
+ if (this.timer !== undefined) this.options.clearTimeout(this.timer);
111
+ this.timer = undefined;
112
+ }
113
+ private invalidate(): void {
114
+ this.generation++; this.pending = undefined; this.clearTimer();
115
+ if (this.active) {
116
+ this.active.controller.abort();
117
+ if (this.active.expiryTimer !== undefined) this.options.clearTimeout(this.active.expiryTimer);
118
+ }
119
+ // Keep active reservation until backend settles, even after abort. Never overlap an uncooperative backend.
120
+ }
121
+ private schedule(): void {
122
+ this.clearTimer();
123
+ if (!this.pending || !this.automaticEnabled()) return;
124
+ const now = this.options.now();
125
+ if (this.pending.pausedAt !== undefined && !this.active?.started) {
126
+ this.pending.expiresAt += now - this.pending.pausedAt;
127
+ this.pending.pausedAt = undefined;
128
+ }
129
+ if (this.pending.pausedAt === undefined && now >= this.pending.expiresAt) { this.pending = undefined; return; }
130
+ if (this.active?.started) {
131
+ // Backend timeout bounds playback. Keep just one candidate, not an accumulating queue.
132
+ this.pending.pausedAt ??= now;
133
+ return;
134
+ }
135
+ const due = Math.max(this.pending.readyAt, this.lastStart + this.settings!.audio.minimumIntervalMs);
136
+ if (!this.active && now >= due) {
137
+ const pending = this.pending; this.pending = undefined;
138
+ this.play(pending.sound, pending.expiresAt); return;
139
+ }
140
+ const wake = this.active ? this.pending.expiresAt : Math.min(due, this.pending.expiresAt);
141
+ this.timer = this.options.setTimeout(() => { this.timer = undefined; this.schedule(); }, wake - now);
142
+ }
143
+ private play(sound: Exclude<NotificationSound, null>, expiresAt?: number): void {
144
+ const active: Active = { controller: new AbortController(), generation: this.generation, started: false };
145
+ this.active = active;
146
+ const permit: PlaybackPermit = { start: () => {
147
+ if (this.active !== active || active.started || active.controller.signal.aborted || this.disposed || active.generation !== this.generation
148
+ || (expiresAt !== undefined && (!this.automaticEnabled() || this.options.now() >= expiresAt))) return false;
149
+ active.started = true; this.lastStart = this.options.now();
150
+ if (active.expiryTimer !== undefined) this.options.clearTimeout(active.expiryTimer);
151
+ this.schedule(); return true;
152
+ } };
153
+ if (expiresAt !== undefined) active.expiryTimer = this.options.setTimeout(() => {
154
+ if (!active.started) active.controller.abort();
155
+ }, expiresAt - this.options.now());
156
+ // Calling synchronously also supports a player that throws before returning its promise.
157
+ try { Promise.resolve(this.options.player(sound, active.controller.signal, permit))
158
+ .then(() => this.finish(active), error => { this.report(error, active); this.finish(active); }); }
159
+ catch (error) { this.report(error, active); this.finish(active); }
160
+ }
161
+ private report(error: unknown, active: Active): void {
162
+ if (active.controller.signal.aborted || active.generation !== this.generation || this.disposed) return;
163
+ const now = this.options.now();
164
+ if (now - this.lastError < 5000) return;
165
+ this.lastError = now;
166
+ try { this.options.onError?.(error); } catch { /* Diagnostics must not produce unhandled rejections. */ }
167
+ }
168
+ private finish(active: Active): void {
169
+ if (active.expiryTimer !== undefined) this.options.clearTimeout(active.expiryTimer);
170
+ if (this.active === active) this.active = undefined;
171
+ this.schedule();
172
+ }
173
+ }
@@ -0,0 +1,202 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { NotificationPlayer, validateNotificationAudio } from "./notification-audio.ts";
3
+ import { constants } from "node:fs";
4
+ import { open } from "node:fs/promises";
5
+ import {
6
+ DEFAULT_NOTIFICATION_SETTINGS, NOTIFICATION_EVENTS, isNotificationSettings, isNotificationSound,
7
+ resolveNotificationSettings, restoreNotificationPreset, writeNotificationSettings,
8
+ type NotificationOptions, type NotificationResolution, type NotificationSettings, type NotificationSound,
9
+ } from "./notification-policy.ts";
10
+ import { NotificationScheduler, type NotificationOccurrence, type SchedulerOptions } from "./notification-scheduler.ts";
11
+
12
+ export interface NotificationServiceDependencies {
13
+ now?: () => number;
14
+ env?: NodeJS.ProcessEnv;
15
+ policy?: NotificationOptions;
16
+ read?: () => NotificationResolution;
17
+ write?: (settings: NotificationSettings) => string;
18
+ backend?: Pick<NotificationPlayer, "play" | "availability" | "capabilities">;
19
+ setTimeout?: SchedulerOptions["setTimeout"];
20
+ clearTimeout?: SchedulerOptions["clearTimeout"];
21
+ }
22
+ export interface NotificationState extends NotificationResolution {
23
+ muted: boolean;
24
+ supportedEvents: readonly NotificationOccurrence["event"][];
25
+ }
26
+ /** UI consumers only retrieve this facade. They never claim a lease or subscribe to events. */
27
+ export interface NotificationService {
28
+ getState(): NotificationState;
29
+ setConfig(ctx: ExtensionContext, settings: NotificationSettings, options?: { confirmRecovery?: boolean }): boolean;
30
+ restorePreset(ctx: ExtensionContext, options?: { confirmRecovery?: boolean }): boolean;
31
+ setMuted(ctx: ExtensionContext, muted: boolean): boolean;
32
+ preview(ctx: ExtensionContext, sound: Exclude<NotificationSound, null>): boolean;
33
+ availability(ctx: ExtensionContext): Promise<"available" | "unavailable">;
34
+ /** Selection-time validation only; never discovers or starts a player. */
35
+ validateFile(ctx: ExtensionContext, sound: `file:${string}`): Promise<boolean>;
36
+ }
37
+ interface Holder {
38
+ muted: boolean;
39
+ epoch: number;
40
+ ownerLease?: symbol;
41
+ retire?: () => void;
42
+ service?: NotificationService;
43
+ startupNotified: boolean;
44
+ sourceSession?: string;
45
+ highwater: Map<string, number>;
46
+ /** A retired scheduler's backend must settle before any replacement can play. */
47
+ playback?: object;
48
+ }
49
+ const HOLDER = Symbol.for("gentle-pi.notification-service.v1");
50
+ function holder(): Holder {
51
+ const root = globalThis as typeof globalThis & { [HOLDER]?: Holder };
52
+ return root[HOLDER] ??= { muted: false, epoch: 0, startupNotified: false, highwater: new Map() };
53
+ }
54
+ export function getNotificationService(): NotificationService | undefined { return holder().service; }
55
+ export function notificationContextAllowed(ctx: ExtensionContext, env: NodeJS.ProcessEnv = process.env): boolean {
56
+ // Dialog-capable interactive RPC is intentionally NOT an audio host.
57
+ return ctx.mode === "tui" && env.GENTLE_PI_AGENTS_CHILD !== "1";
58
+ }
59
+
60
+ /** Owner-only claim; factory collision invalidates old hooks before the new owner attaches. */
61
+ export function claimNotificationOwner(retireRuntime: () => void, deps: NotificationServiceDependencies = {}) {
62
+ const shared = holder(); shared.retire?.();
63
+ const lease = Symbol("notification-owner"); const epoch = ++shared.epoch;
64
+ shared.ownerLease = lease;
65
+ const now = deps.now ?? (() => performance.now());
66
+ const env = deps.env ?? process.env;
67
+ const backend = deps.backend ?? new NotificationPlayer();
68
+ let ctx: ExtensionContext | undefined; let sessionId: string | undefined;
69
+ let scheduler: NotificationScheduler | undefined; let disposed = false; let attachment = 0;
70
+ let resolution: NotificationResolution | undefined;
71
+ const diagnostics = new Set<string>();
72
+ const current = () => !disposed && shared.ownerLease === lease && shared.epoch === epoch;
73
+ const allowed = (candidate?: ExtensionContext) => current() && !!ctx && !!candidate
74
+ && notificationContextAllowed(candidate, env) && notificationContextAllowed(ctx, env)
75
+ && candidate.sessionManager.getSessionId() === sessionId && ctx.sessionManager.getSessionId() === sessionId;
76
+ const report = (category: string, message: string) => {
77
+ if (!allowed(ctx) || diagnostics.has(category)) return;
78
+ diagnostics.add(category);
79
+ try { ctx!.ui.notify(message, "warning"); } catch { /* Local diagnostics cannot fail a run. */ }
80
+ };
81
+ const read = () => {
82
+ resolution = structuredClone((deps.read ?? (() => resolveNotificationSettings(deps.policy)))());
83
+ if (resolution.malformed || resolution.readError) {
84
+ resolution.settings = structuredClone(DEFAULT_NOTIFICATION_SETTINGS);
85
+ report("config", "Notification configuration unavailable or invalid; audio disabled.");
86
+ }
87
+ };
88
+ const makeScheduler = () => {
89
+ scheduler = new NotificationScheduler({ now,
90
+ // SAFETY: scheduler treats this handle as opaque; Node's Timeout is returned unchanged to clearTimeout.
91
+ setTimeout: deps.setTimeout ?? ((fn, ms) => setTimeout(fn, ms) as unknown as number),
92
+ clearTimeout: deps.clearTimeout ?? (id => clearTimeout(id)),
93
+ player: async (sound, signal, permit) => {
94
+ if (!allowed(ctx) || shared.playback || signal.aborted) return;
95
+ const reservation = {}; shared.playback = reservation;
96
+ try {
97
+ const available = await backend.availability();
98
+ if (signal.aborted || !allowed(ctx)) return;
99
+ if (available === "unavailable") { report("backend", "Notification audio backend unavailable."); return; }
100
+ await backend.play(sound, signal, { start: () => allowed(ctx) && permit.start() });
101
+ } finally { if (shared.playback === reservation) shared.playback = undefined; }
102
+ },
103
+ onError: () => report("playback", "Notification audio could not be played."),
104
+ });
105
+ scheduler.resetSession(sessionId!); scheduler.configure(resolution!.settings, shared.muted);
106
+ };
107
+ const shutdown = () => {
108
+ scheduler?.dispose(); scheduler = undefined; ctx = undefined; sessionId = undefined;
109
+ attachment++;
110
+ if (shared.ownerLease === lease) shared.service = undefined;
111
+ };
112
+ const service: NotificationService = {
113
+ getState: () => ({ ...structuredClone(resolution!), muted: shared.muted,
114
+ // Quit cannot await lazy detection/playback without delaying cleanup/exit.
115
+ supportedEvents: NOTIFICATION_EVENTS.filter(event => event !== "session.shutdown") }),
116
+ setConfig: (candidate, settings, options = {}) => {
117
+ if (!allowed(candidate) || !resolution || !isNotificationSettings(settings, deps.policy?.pathFlavor)
118
+ || ((resolution.malformed || resolution.readError) && options.confirmRecovery !== true)) return false;
119
+ try {
120
+ const snapshot = structuredClone(settings);
121
+ const globalFile = (deps.write ?? (value => writeNotificationSettings(value, deps.policy)))(snapshot);
122
+ // Invalidate even enabled->enabled changes: pending mappings must not become stale.
123
+ scheduler?.dispose();
124
+ resolution = { settings: snapshot, globalFile, source: "global_file", malformed: false, readError: false };
125
+ makeScheduler(); return true;
126
+ } catch { report("write", "Notification configuration could not be saved."); return false; }
127
+ },
128
+ restorePreset: (candidate, options) => service.setConfig(candidate, restoreNotificationPreset(resolution!.settings), options),
129
+ setMuted: (candidate, muted) => {
130
+ if (!allowed(candidate) || typeof muted !== "boolean") return false;
131
+ shared.muted = muted; scheduler?.configure(resolution!.settings, muted); return true;
132
+ },
133
+ preview: (candidate, sound) => allowed(candidate) && !shared.playback && isNotificationSound(sound, deps.policy?.pathFlavor)
134
+ && sound !== null && !!scheduler?.preview(sound),
135
+ validateFile: async (candidate, sound) => {
136
+ if (!allowed(candidate) || !isNotificationSound(sound) || !sound.startsWith("file:")) return false;
137
+ const activeScheduler = scheduler;
138
+ try {
139
+ const handle = await open(sound.slice(5), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
140
+ try {
141
+ const stat = await handle.stat();
142
+ if (!stat.isFile() || stat.size < 44 || stat.size > 2 * 1024 * 1024) return false;
143
+ const bytes = Buffer.alloc(stat.size + 1);
144
+ const { bytesRead } = await handle.read(bytes);
145
+ if (bytesRead !== stat.size) return false;
146
+ const { format } = validateNotificationAudio(bytes.subarray(0, bytesRead));
147
+ // Assignment-time rejection: a player that cannot play this container must never receive it.
148
+ if (format !== "wav" && !(await backend.capabilities()).has(format)) return false;
149
+ return allowed(candidate) && scheduler === activeScheduler;
150
+ } finally { await handle.close(); }
151
+ } catch { return false; }
152
+ },
153
+ availability: async candidate => {
154
+ if (!allowed(candidate)) return "unavailable";
155
+ const activeScheduler = scheduler;
156
+ try {
157
+ const result = await backend.availability();
158
+ return allowed(candidate) && scheduler === activeScheduler ? result : "unavailable";
159
+ } catch { return "unavailable"; }
160
+ },
161
+ };
162
+ const bindService = (): NotificationService => {
163
+ const generation = attachment;
164
+ const live = () => generation === attachment && current();
165
+ return {
166
+ getState: service.getState,
167
+ setConfig: (candidate, settings, options) => live() && service.setConfig(candidate, settings, options),
168
+ restorePreset: (candidate, options) => live() && service.restorePreset(candidate, options),
169
+ setMuted: (candidate, muted) => live() && service.setMuted(candidate, muted),
170
+ preview: (candidate, sound) => live() && service.preview(candidate, sound),
171
+ availability: candidate => live() ? service.availability(candidate) : Promise.resolve("unavailable"),
172
+ validateFile: (candidate, sound) => live() ? service.validateFile(candidate, sound) : Promise.resolve(false),
173
+ };
174
+ };
175
+ const retire = () => {
176
+ if (disposed) return;
177
+ shutdown(); disposed = true; retireRuntime();
178
+ if (shared.ownerLease === lease) { shared.ownerLease = undefined; shared.retire = undefined; }
179
+ };
180
+ shared.retire = retire;
181
+ return { now, current, allowed, shutdown, retire,
182
+ acceptSource: (producerId: string, runId: string, sequence: number) => {
183
+ if (!allowed(ctx)) return false;
184
+ const key = JSON.stringify([producerId, runId]);
185
+ if (sequence <= (shared.highwater.get(key) ?? 0)) return false;
186
+ shared.highwater.set(key, sequence); return true;
187
+ },
188
+ attach: (candidate: ExtensionContext) => {
189
+ if (!current()) return false;
190
+ shutdown();
191
+ if (!notificationContextAllowed(candidate, env)) return false;
192
+ ctx = candidate; sessionId = candidate.sessionManager.getSessionId();
193
+ if (shared.sourceSession !== sessionId) { shared.highwater.clear(); shared.sourceSession = sessionId; }
194
+ read(); makeScheduler(); shared.service = bindService(); return true;
195
+ },
196
+ enqueue: (event: NotificationOccurrence) => allowed(ctx) && scheduler?.enqueue(event),
197
+ startup: () => {
198
+ if (!allowed(ctx) || shared.startupNotified) return false;
199
+ shared.startupNotified = true; return true;
200
+ },
201
+ };
202
+ }
@@ -0,0 +1,87 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { BUILTIN_NOTIFICATION_IDS, isNotificationSound, type NotificationEvent, type NotificationSettings, type NotificationSound } from "./notification-policy.ts";
3
+ import { getNotificationService, notificationContextAllowed, type NotificationService } from "./notification-service.ts";
4
+
5
+ /**
6
+ * Native dialogs, outside the transcript. No owner creation, commands or visual-store writes.
7
+ * @deprecated Superseded by the direct rows in `lib/notification-customize.ts`; kept as an
8
+ * unused test-covered fallback, not wired into `/gentle:customize` anymore.
9
+ */
10
+ export async function openNotificationPanel(ctx: ExtensionContext): Promise<void> {
11
+ if (!ctx.hasUI || !notificationContextAllowed(ctx)) return;
12
+ const fresh = (expected: NotificationService) => {
13
+ const service = getNotificationService();
14
+ return service === expected ? service : undefined;
15
+ };
16
+ const notify = (message: string, error = false) => ctx.ui.notify(message, error ? "warning" : "info");
17
+ const chooseSound = async (expected: NotificationService): Promise<NotificationSound | undefined> => {
18
+ const choice = await ctx.ui.select("Notification sound", ["Silence", ...BUILTIN_NOTIFICATION_IDS.map(id => `builtin:${id}`), "Local WAV file"]);
19
+ if (!fresh(expected) || choice === undefined) return undefined;
20
+ if (choice === "Silence") return null;
21
+ if (choice !== "Local WAV file") return isNotificationSound(choice) ? choice : undefined;
22
+ const path = await ctx.ui.input("Local WAV file", "Absolute path (no URLs or shell expansion)");
23
+ const service = fresh(expected);
24
+ if (!service || path === undefined) return undefined;
25
+ const sound = `file:${path}` as const;
26
+ if (!isNotificationSound(sound) || !await service.validateFile(ctx, sound)) {
27
+ if (fresh(expected)) notify("Select a readable regular PCM WAV (≤2 MiB, ≤10 seconds); absolute local path only.", true);
28
+ return undefined;
29
+ }
30
+ return fresh(expected) ? sound : undefined;
31
+ };
32
+ const save = async (expected: NotificationService, change?: (settings: NotificationSettings) => void) => {
33
+ let service = fresh(expected);
34
+ if (!service) return;
35
+ const state = service.getState();
36
+ let confirmRecovery = false;
37
+ if (state.malformed || state.readError) {
38
+ confirmRecovery = await ctx.ui.confirm("Replace invalid notification configuration?",
39
+ "The existing configuration is invalid or unreadable. Replace it with these settings? Cancellation leaves it untouched.") === true;
40
+ if (!confirmRecovery) return;
41
+ }
42
+ service = fresh(expected);
43
+ if (!service) return;
44
+ // Read again after all prompts: never overwrite another action with an old snapshot.
45
+ const settings = service.getState().settings;
46
+ change?.(settings);
47
+ const saved = change ? service.setConfig(ctx, settings, { confirmRecovery }) : service.restorePreset(ctx, { confirmRecovery });
48
+ notify(saved ? "Notification preferences saved." : "Notification preferences not saved; choose again to retry.", !saved);
49
+ };
50
+ while (true) {
51
+ const service = getNotificationService();
52
+ if (!service) return;
53
+ const state = service.getState();
54
+ const enable = state.settings.enabled ? "Disable audio" : "Enable audio";
55
+ const mute = state.muted ? "Resume" : "Mute";
56
+ const action = await ctx.ui.select(`Notifications · audio ${state.settings.enabled ? "on" : "off"} · ${state.muted ? "muted" : "unmuted"}`,
57
+ [enable, "Assign sound", "Preview sound", mute, "Check availability", "Restore preset"]);
58
+ if (!fresh(service) || action === undefined) return;
59
+ try {
60
+ if (action === enable) await save(service, settings => { settings.enabled = action === "Enable audio"; });
61
+ else if (action === mute) fresh(service)?.setMuted(ctx, action === "Mute");
62
+ else if (action === "Restore preset") await save(service);
63
+ else if (action === "Check availability") {
64
+ const result = await fresh(service)!.availability(ctx);
65
+ if (fresh(service)) notify(`Notification audio backend: ${result}.`);
66
+ } else if (action === "Preview sound") {
67
+ const sound = await chooseSound(service);
68
+ const current = fresh(service);
69
+ if (current && sound !== undefined && sound !== null && !current.preview(ctx, sound)) notify("Preview unavailable or audio busy; try again.", true);
70
+ } else if (action === "Assign sound") {
71
+ const origins = [...new Set(service.getState().supportedEvents.map(event => event.split(".")[0]!))];
72
+ const origin = await ctx.ui.select("Notification origin", origins);
73
+ let current = fresh(service);
74
+ if (!current || origin === undefined || !origins.includes(origin)) continue;
75
+ const events = current.getState().supportedEvents.filter(event => event.startsWith(`${origin}.`));
76
+ const event = await ctx.ui.select("Notification event", [...events]);
77
+ current = fresh(service);
78
+ if (!current || !events.includes(event as NotificationEvent)) continue;
79
+ const sound = await chooseSound(service);
80
+ if (sound !== undefined) await save(service, settings => { settings.audio.events[event as NotificationEvent] = sound; });
81
+ }
82
+ } catch {
83
+ if (fresh(service)) notify("Notification action failed; no automatic retry.", true);
84
+ }
85
+ if (!fresh(service)) return;
86
+ }
87
+ }