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
@@ -994,11 +994,80 @@ function operationForTransition(transition: ReviewTransition): ReviewOperation {
994
994
  throw new ReviewIntegrityError(`Unsupported reducer transition: ${transition}`);
995
995
  }
996
996
 
997
+ function assertReducerInput(transition: ReviewTransition, input: unknown): asserts input is ReviewReducerInput {
998
+ const fail = (field: string): never => { throw new ReviewIntegrityError(`Graph reducer input ${field} is invalid`); };
999
+ const object = (value: unknown, field: string): Record<string, unknown> => {
1000
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return fail(field);
1001
+ return value as Record<string, unknown>;
1002
+ };
1003
+ const string = (value: unknown, field: string): void => { if (typeof value !== "string") fail(field); };
1004
+ const boolean = (value: unknown, field: string): void => { if (typeof value !== "boolean") fail(field); };
1005
+ const strings = (value: unknown, field: string): void => { if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) fail(field); };
1006
+ const rows = (value: unknown, field: string): void => {
1007
+ if (!Array.isArray(value)) return fail(field);
1008
+ // The canonical row decoder checks every field, enum and duplicate ID.
1009
+ createFrozenLedger(value);
1010
+ };
1011
+ const results = (value: unknown, field: string): void => {
1012
+ if (!Array.isArray(value)) return fail(field);
1013
+ for (const item of value) {
1014
+ const result = object(item, field);
1015
+ string(result.id, `${field}.id`);
1016
+ if (!Object.values(RESOLUTION_OUTCOME).some((outcome) => outcome === result.outcome)) fail(`${field}.outcome`);
1017
+ }
1018
+ };
1019
+ const body = object(input, "object");
1020
+ switch (transition) {
1021
+ case REVIEW_TRANSITION.ORDINARY_DISCOVERY: rows(body.rows, "rows"); break;
1022
+ case REVIEW_TRANSITION.JUDGMENT_DAY_DISCOVERY: rows(body.judgeA, "judgeA"); rows(body.judgeB, "judgeB"); break;
1023
+ case REVIEW_TRANSITION.ORDINARY_EVIDENCE:
1024
+ results(body.deterministicResults, "deterministicResults");
1025
+ if (body.refuterResults !== undefined) results(body.refuterResults, "refuterResults");
1026
+ break;
1027
+ case REVIEW_TRANSITION.ORDINARY_FIX:
1028
+ case REVIEW_TRANSITION.JUDGMENT_DAY_FIX:
1029
+ string(body.candidateTree, "candidateTree"); strings(body.fixedIds, "fixedIds"); string(body.fixDiff, "fixDiff");
1030
+ if (body.changedPaths !== undefined) strings(body.changedPaths, "changedPaths");
1031
+ break;
1032
+ case REVIEW_TRANSITION.ORDINARY_NO_FIX: string(body.reason, "reason"); break;
1033
+ case REVIEW_TRANSITION.ORDINARY_FINAL_VERIFICATION:
1034
+ case REVIEW_TRANSITION.JUDGMENT_DAY_FINAL_VERIFICATION:
1035
+ boolean(body.passed, "passed"); if (body.reason !== undefined) string(body.reason, "reason"); break;
1036
+ case REVIEW_TRANSITION.ORDINARY_VALIDATION:
1037
+ case REVIEW_TRANSITION.JUDGMENT_DAY_REJUDGMENT: {
1038
+ const request = object(body.request, "request");
1039
+ strings(request.requested_ids, "request.requested_ids"); rows(request.frozen_rows, "request.frozen_rows"); string(request.frozen_ledger_hash, "request.frozen_ledger_hash");
1040
+ if (transition === REVIEW_TRANSITION.JUDGMENT_DAY_REJUDGMENT) {
1041
+ for (const field of ["fix_diff", "fix_diff_hash", "candidate_tree"]) string(request[field], `request.${field}`);
1042
+ if (typeof request.round !== "number" || !Number.isSafeInteger(request.round) || request.round < 1) fail("request.round");
1043
+ results(body.judgeAResults, "judgeAResults"); results(body.judgeBResults, "judgeBResults");
1044
+ } else {
1045
+ results(body.results, "results");
1046
+ const acceptance = object(request.original_acceptance_tests, "request.original_acceptance_tests");
1047
+ boolean(acceptance.passed, "request.original_acceptance_tests.passed"); string(acceptance.evidence_hash, "request.original_acceptance_tests.evidence_hash");
1048
+ strings(request.original_criterion_regressions, "request.original_criterion_regressions");
1049
+ if (!Array.isArray(request.correction_regressions) || !Array.isArray(request.follow_ups)) return fail("request.validation arrays");
1050
+ for (const value of request.correction_regressions) {
1051
+ const regression = object(value, "request.correction_regressions");
1052
+ string(regression.finding_id, "regression.finding_id"); string(regression.evidence_hash, "regression.evidence_hash"); boolean(regression.passed, "regression.passed");
1053
+ }
1054
+ for (const value of request.follow_ups) {
1055
+ const followUp = object(value, "request.follow_ups");
1056
+ for (const field of ["id", "location", "summary", "evidence_hash"]) string(followUp[field], `follow_up.${field}`);
1057
+ }
1058
+ }
1059
+ break;
1060
+ }
1061
+ default: fail("transition");
1062
+ }
1063
+ }
1064
+
997
1065
  function reduceReviewState(
998
1066
  state: ReviewStateV1,
999
1067
  transition: ReviewTransition,
1000
- input: ReviewReducerInput,
1068
+ input: unknown,
1001
1069
  ): ReviewStateV1 {
1070
+ assertReducerInput(transition, input);
1002
1071
  switch (transition) {
1003
1072
  case REVIEW_TRANSITION.ORDINARY_DISCOVERY:
1004
1073
  return recordOrdinaryDiscovery(state, input as OrdinaryDiscoveryInput);
@@ -1456,8 +1525,8 @@ export class ReviewTransactionStore {
1456
1525
  const existing = current ? (current.body.lineages as Array<Record<string, unknown>>).find((value) => value.lineage_id === next.lineage_id && value.mode === "graph") : undefined;
1457
1526
  if (previous && !existing) throw new ReviewIntegrityError("Graph predecessor is missing");
1458
1527
  if (!previous && existing) throw new ReviewIntegrityError("Graph lineage already exists");
1459
- const predecessor = existing?.head_event_id;
1460
- if (predecessor !== undefined && typeof predecessor !== "string") throw new ReviewIntegrityError("Graph head is invalid");
1528
+ if (existing?.head_event_id !== undefined && typeof existing.head_event_id !== "string") throw new ReviewIntegrityError("Graph head is invalid");
1529
+ const predecessor = typeof existing?.head_event_id === "string" ? existing.head_event_id : undefined;
1461
1530
  const last = next.request_journal.at(-1);
1462
1531
  const descriptor = (() => { try { return graph.readStoreDescriptor(); } catch { return undefined; } })();
1463
1532
  const reducerTransition = eventContext?.transition ?? (predecessor === undefined ? "start" : last?.operation ?? "state-update");
@@ -18,7 +18,7 @@ const tokenFields = ["input", "output", "cacheRead", "cacheWrite", "reasoning",
18
18
  * Only the fixed package catalog is cached; runtime instructions are not retained.
19
19
  */
20
20
  let definitions: Array<{ name: string; fingerprint: string; fingerprintClass?: AgentClass }> | undefined;
21
- const packagedAgentClassAliases = new Map([["sdd-proposal", "sdd-propose"]] as const);
21
+ const packagedAgentClassAliases = new Map<string, string>([["sdd-proposal", "sdd-propose"]]);
22
22
  function fingerprintAgentClassName(name: string): string {
23
23
  const compatibilityName = name.startsWith("gentle-ai-") ? name.slice("gentle-ai-".length) : name;
24
24
  return packagedAgentClassAliases.get(compatibilityName) ?? compatibilityName;
@@ -1,5 +1,5 @@
1
1
  import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from "node:child_process";
2
- import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
2
+ import { gentleAiIsolationEnv, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
3
3
  import { runtimeMetricsEnvAllows } from "./runtime-metrics-policy.ts";
4
4
  import { isPublicRuntimeModelId, normalizeRuntimeModel, type RuntimeMetricBucket } from "./runtime-metrics.ts";
5
5
  import type { ChildLaunchBucket } from "./runtime-metrics-children.ts";
@@ -105,7 +105,8 @@ export async function sendNativeRuntimeEvent(rows: readonly RuntimeMetricBucket[
105
105
  const file = (deps.resolve ?? resolveGentleAiBinary)();
106
106
  if (!runtimeMetricsEnvAllows(env)) return "disabled";
107
107
  if (deps.signal?.aborted || deps.current?.() === false) return "discarded";
108
- return await sendProcess(file, cwd, payload, env, deps.spawn ?? spawn, deps.signal);
108
+ // S9: a bundled install keeps Gentle AI's state inside its prefix.
109
+ return await sendProcess(file, cwd, payload, gentleAiIsolationEnv(env), deps.spawn ?? spawn, deps.signal);
109
110
  } catch { return "discarded"; }
110
111
  finally { busy = false; }
111
112
  }
@@ -0,0 +1,527 @@
1
+ import { decodePrintableKey } from "./pi-tui-keys.ts";
2
+ import { isKeyRelease, matchesKey, truncateToWidth, visibleWidth, type EditorComponent } from "@earendil-works/pi-tui";
3
+
4
+ /**
5
+ * Native text-selection engine for GentlePromptEditor: shift+home / shift+end
6
+ * selection, alt+e / alt+a select-all, and replace-on-key (backspace / delete /
7
+ * printable character) with reverse-video highlight and a bottom-rule hint.
8
+ * Ported from @exopro/pi-select-del so the petal prompt owns the feature
9
+ * natively — no factory composition, no cross-extension focus handoff.
10
+ *
11
+ * Behavior contract (ported from pi-select-del, with atomic paste replacement):
12
+ * - shift+home — anchor at the cursor, move to line start (held presses at
13
+ * the edge keep the selection; only a zero-width span collapses)
14
+ * - shift+end — anchor at the cursor, move to line end
15
+ * - alt+e — select all; alt+a too when no extension shortcut claims it
16
+ * (Gentle Agents owns alt+a by default, gentle-shell#1565)
17
+ * - a registered extension shortcut always wins over these selection chords,
18
+ * matching CustomEditor's own precedence
19
+ * - backspace / delete / printable character or bracketed paste over an
20
+ * active selection — replace it in one atomic edit (undo restores text AND cursor)
21
+ * - any other key — collapse the selection first, then behave natively
22
+ *
23
+ * The engine drives the host editor's own internals (the EditorInternals
24
+ * surface every CustomEditor-derived editor exposes) and degrades to pure
25
+ * passthrough if that surface drifts: a pi upgrade can cost the selection
26
+ * features, never crash editing.
27
+ */
28
+
29
+ /** Cursor/anchor position in logical (line, col) editor coordinates. */
30
+ export interface Point {
31
+ line: number;
32
+ col: number;
33
+ }
34
+
35
+ /**
36
+ * Narrow view of the private Editor internals the engine relies on. Kept in
37
+ * one place so a pi upgrade only needs re-verifying against this interface.
38
+ */
39
+ export interface EditorInternals {
40
+ state: { lines: string[]; cursorLine: number; cursorCol: number };
41
+ paddingX: number;
42
+ scrollOffset: number;
43
+ renderedVisibleLineCount: number;
44
+ /** Layout width the last base render wrapped at; soft-optional — falsy falls back to manual width math. */
45
+ lastWidth: number;
46
+ tui: { requestRender(): void };
47
+ autocompleteState: "regular" | "force" | null;
48
+ lastAction: "kill" | "yank" | "type-word" | null;
49
+ pushUndoSnapshot(): void;
50
+ setCursorCol(col: number): void;
51
+ moveToLineStart(): void;
52
+ moveToLineEnd(): void;
53
+ exitHistoryBrowsing(): void;
54
+ cancelAutocomplete(): void;
55
+ updateAutocomplete(): void;
56
+ buildVisualLineMap(width: number): Array<{ logicalLine: number; startCol: number; length: number }>;
57
+ }
58
+
59
+ const INTERNAL_PROBE_MEMBERS = {
60
+ properties: [
61
+ "state",
62
+ "paddingX",
63
+ "scrollOffset",
64
+ "renderedVisibleLineCount",
65
+ "autocompleteState",
66
+ "lastAction",
67
+ "tui",
68
+ ],
69
+ functions: [
70
+ "pushUndoSnapshot",
71
+ "setCursorCol",
72
+ "moveToLineStart",
73
+ "moveToLineEnd",
74
+ "exitHistoryBrowsing",
75
+ "cancelAutocomplete",
76
+ "updateAutocomplete",
77
+ "buildVisualLineMap",
78
+ ],
79
+ } as const satisfies {
80
+ properties: readonly (keyof EditorInternals)[];
81
+ functions: readonly (keyof EditorInternals)[];
82
+ };
83
+
84
+ /**
85
+ * Members of `target` missing or malformed against the EditorInternals
86
+ * contract. Class-field trap: the properties are ES class fields (instance
87
+ * own-properties), invisible to any prototype probe — probe a real instance,
88
+ * never Editor.prototype or a subclass prototype.
89
+ */
90
+ export function missingEditorInternals(target: object): string[] {
91
+ const missing: string[] = [];
92
+ const view = target as Record<string, unknown>;
93
+ for (const name of INTERNAL_PROBE_MEMBERS.properties) {
94
+ if (view[name] === undefined) missing.push(name);
95
+ }
96
+ for (const name of INTERNAL_PROBE_MEMBERS.functions) {
97
+ if (typeof view[name] !== "function") missing.push(name);
98
+ }
99
+ return missing;
100
+ }
101
+
102
+ export function clamp(value: number, lo: number, hi: number): number {
103
+ return Math.max(lo, Math.min(hi, value));
104
+ }
105
+
106
+ /**
107
+ * Wrap the code-unit span [startCu, endCu) of a rendered editor row in reverse
108
+ * video. Positions are code-unit offsets into the row's PLAIN text (same unit
109
+ * the editor uses for cursorCol and visual-line map columns). The rendered row
110
+ * may already contain escape sequences (SGR colors, cursor markers); the walk
111
+ * passes those through untouched, keeping code-unit alignment. The span closes
112
+ * with SGR 27 (reverse off), not a full reset: row attributes set before the
113
+ * span must survive. A nested SGR reset re-arms reverse right after it.
114
+ */
115
+ export function withReverseSpan(row: string, startCu: number, endCu: number): string {
116
+ let out = "";
117
+ let cu = 0;
118
+ let i = 0;
119
+ let opened = false;
120
+ while (i < row.length) {
121
+ if (!opened && cu >= startCu) {
122
+ out += "\x1b[7m";
123
+ opened = true;
124
+ }
125
+ if (opened && cu >= endCu) {
126
+ return `${out}\x1b[27m${row.slice(i)}`;
127
+ }
128
+ if (row[i] === "\x1b") {
129
+ const seq = row.slice(i, i + escapeSequenceLength(row, i));
130
+ out += seq;
131
+ // A nested SGR reset (e.g. the cursor block's own, when the cursor
132
+ // sits inside the span) clears reverse for everything after it:
133
+ // re-arm reverse right after it.
134
+ if (opened && cu < endCu && isSgrReset(seq)) out += "\x1b[7m";
135
+ i += seq.length;
136
+ continue;
137
+ }
138
+ out += row[i];
139
+ i += 1;
140
+ cu += 1;
141
+ }
142
+ return opened ? `${out}\x1b[27m` : out;
143
+ }
144
+
145
+ /** Length of the escape sequence at s[i] (s[i] === ESC). Unterminated sequences end the row. */
146
+ export function escapeSequenceLength(s: string, i: number): number {
147
+ const next = s[i + 1];
148
+ if (next === "[") {
149
+ // CSI: parameter/intermediate bytes 0x20-0x3F, final byte 0x40-0x7E.
150
+ for (let j = i + 2; j < s.length; j++) {
151
+ const code = s.charCodeAt(j);
152
+ if (code >= 0x40 && code <= 0x7e) return j - i + 1;
153
+ }
154
+ return s.length - i;
155
+ }
156
+ if (next === "]" || next === "_") {
157
+ // OSC / APC (cursor markers are APC): terminated by BEL or ST (ESC \).
158
+ const bel = s.indexOf("\x07", i + 2);
159
+ const st = s.indexOf("\x1b\\", i + 2);
160
+ if (bel === -1 && st === -1) return s.length - i;
161
+ if (bel === -1) return st - i + 2;
162
+ if (st === -1) return bel - i + 1;
163
+ return Math.min(bel - i + 1, st - i + 2);
164
+ }
165
+ return 2;
166
+ }
167
+
168
+ /** True for SGR reset sequences: CSI ... m with an empty or all-zero parameter list. */
169
+ export function isSgrReset(seq: string): boolean {
170
+ const match = /^\x1b\[([\d;]*)m$/.exec(seq);
171
+ if (!match) return false;
172
+ const params = match[1];
173
+ if (params === "") return true;
174
+ return /^0+$/.test(params.split(";")[0]);
175
+ }
176
+
177
+ /**
178
+ * Selection engine driving a host editor through the EditorInternals cast.
179
+ * `handleInput` takes the host's native dispatch as a `native` callback so the
180
+ * host keeps its own key chain (Esc gates, autocomplete, history) intact:
181
+ * selection keys are handled here; everything else collapses the anchor first,
182
+ * then behaves natively.
183
+ */
184
+ const PASTE_START = "\x1b[200~";
185
+ const PASTE_END = "\x1b[201~";
186
+ const PASTE_END_TAIL_LENGTH = PASTE_END.length - 1;
187
+ /**
188
+ * Safety bound (UTF-8 bytes) for a bracketed-paste frame that has not yet
189
+ * seen its terminator. Checked only against a per-chunk byte count (never a
190
+ * full-buffer rescan), so it costs one Buffer.byteLength call per NEW chunk.
191
+ * Never applied once the terminator is found: a complete frame of any size,
192
+ * even one exceeding this bound, still reaches replaceSelectionWithPaste with
193
+ * one-step undo intact.
194
+ */
195
+ const MAX_INCOMPLETE_PASTE_BYTES = 16 * 1024 * 1024;
196
+
197
+ export class SelectionEngine {
198
+ /** Selection anchor in logical (line, col) coordinates; adapter-exposed state. */
199
+ anchor: Point | null = null;
200
+ /**
201
+ * Chunks of an in-progress bracketed-paste frame (undefined when none is
202
+ * open). Chunks are only ever appended, never re-concatenated, so buffering
203
+ * N chunks stays O(N) total; the full frame is joined once, at completion.
204
+ */
205
+ private pasteChunks: string[] | undefined;
206
+ /** Total code-unit length of `pasteChunks` before the chunk being processed; used to locate the terminator inside the joined frame. */
207
+ private pasteBufferedLength = 0;
208
+ /** Total UTF-8 byte length buffered so far while no terminator has been seen; compared against MAX_INCOMPLETE_PASTE_BYTES. */
209
+ private pasteBufferedBytes = 0;
210
+ /**
211
+ * Up to the last `PASTE_END_TAIL_LENGTH` characters buffered so far, carried
212
+ * forward so the terminator can be found even when split across chunk
213
+ * boundaries, without rescanning every previously buffered chunk.
214
+ */
215
+ private pasteTail = "";
216
+
217
+ /**
218
+ * Capability probe cache for the host internals: null until first use, then
219
+ * the (possibly empty) list of missing members, computed once on the first
220
+ * handleInput/render call. Any defect permanently DEGRADES the host to
221
+ * passthrough — selection features off, never a crash.
222
+ */
223
+ private internalDefects: string[] | null = null;
224
+
225
+ private readonly editor: EditorComponent;
226
+
227
+ constructor(editor: EditorComponent) {
228
+ this.editor = editor;
229
+ }
230
+
231
+ /** True once the probe found missing internals; computes and caches the probe on first call. */
232
+ get degraded(): boolean {
233
+ if (this.internalDefects === null) {
234
+ this.internalDefects = missingEditorInternals(this.editor);
235
+ }
236
+ return this.internalDefects.length > 0;
237
+ }
238
+
239
+ private get internals(): EditorInternals {
240
+ return this.editor as unknown as EditorInternals;
241
+ }
242
+
243
+ private get s(): EditorInternals["state"] {
244
+ return this.internals.state;
245
+ }
246
+
247
+ private cursor(): Point {
248
+ return { line: this.s.cursorLine, col: this.s.cursorCol };
249
+ }
250
+
251
+ private setCursor(p: Point): void {
252
+ this.s.cursorLine = p.line;
253
+ this.internals.setCursorCol(p.col);
254
+ }
255
+
256
+ /** Ordered (start, end) selection range, or null when no anchor is set. */
257
+ range(): [Point, Point] | null {
258
+ if (!this.anchor) return null;
259
+ const a = this.anchor;
260
+ const c = this.cursor();
261
+ const anchorFirst = a.line < c.line || (a.line === c.line && a.col < c.col);
262
+ return anchorFirst ? [a, c] : [c, a];
263
+ }
264
+
265
+ /** Number of characters covered by the active selection (0 when none). Counts line breaks a deletion would remove. */
266
+ selectionLength(): number {
267
+ const range = this.range();
268
+ if (!range) return 0;
269
+ const [start, end] = range;
270
+ const lines = this.s.lines;
271
+ if (start.line === end.line) return end.col - start.col;
272
+ let n = (lines[start.line] ?? "").length - start.col;
273
+ for (let i = start.line + 1; i < end.line; i++) n += (lines[i] ?? "").length;
274
+ return n + end.col + (end.line - start.line);
275
+ }
276
+
277
+ /**
278
+ * Selection dispatch in front of the native chain. Selection and replace
279
+ * keys are handled here; everything else collapses the anchor first, then
280
+ * behaves natively. Degraded hosts keep pure native key handling.
281
+ * `shortcut` probes registered extension shortcuts before a selection chord
282
+ * is consumed; it returns true when a shortcut handled the key.
283
+ */
284
+ handleInput(data: string, native: (data: string) => void, shortcut?: (data: string) => boolean): void {
285
+ if (this.degraded) {
286
+ native(data);
287
+ return;
288
+ }
289
+ if (this.pasteChunks !== undefined || (this.anchor && data.startsWith(PASTE_START))) {
290
+ const priorLength = this.pasteBufferedLength;
291
+ const window = this.pasteTail + data;
292
+ const idx = window.indexOf(PASTE_END);
293
+ const chunks = (this.pasteChunks ??= []);
294
+ chunks.push(data);
295
+ this.pasteBufferedLength = priorLength + data.length;
296
+ if (idx < 0) {
297
+ this.pasteBufferedBytes += Buffer.byteLength(data, "utf8");
298
+ if (this.pasteBufferedBytes > MAX_INCOMPLETE_PASTE_BYTES) {
299
+ // Never seen a terminator and the buffered frame outgrew the safety
300
+ // bound: abandon it. The host editor started its own paste buffer at
301
+ // PASTE_START and is still waiting for PASTE_END, so forwarding the
302
+ // incomplete bytes to `native` could let it swallow later keystrokes
303
+ // — drop them instead of forwarding anything.
304
+ this.pasteChunks = undefined;
305
+ this.pasteBufferedLength = 0;
306
+ this.pasteBufferedBytes = 0;
307
+ this.pasteTail = "";
308
+ return;
309
+ }
310
+ this.pasteTail = window.slice(-PASTE_END_TAIL_LENGTH);
311
+ return;
312
+ }
313
+ const frameEnd = priorLength - this.pasteTail.length + idx + PASTE_END.length;
314
+ const buffered = chunks.join("");
315
+ const frame = buffered.slice(0, frameEnd);
316
+ const remaining = buffered.slice(frameEnd);
317
+ this.pasteChunks = undefined;
318
+ this.pasteBufferedLength = 0;
319
+ this.pasteBufferedBytes = 0;
320
+ this.pasteTail = "";
321
+ this.replaceSelectionWithPaste(frame, native);
322
+ if (remaining) this.handleInput(remaining, native, shortcut);
323
+ return;
324
+ }
325
+ // Kitty flag 2 release byte strings still match their own key, so
326
+ // releases must be dropped before any matchesKey.
327
+ if (isKeyRelease(data)) return;
328
+ if (this.isSelectionChord(data) && shortcut?.(data)) return;
329
+ if (matchesKey(data, "shift+home")) {
330
+ this.selectToLineEdge(false);
331
+ return;
332
+ }
333
+ if (matchesKey(data, "shift+end")) {
334
+ this.selectToLineEdge(true);
335
+ return;
336
+ }
337
+ if (matchesKey(data, "alt+e") || matchesKey(data, "alt+a")) {
338
+ this.selectAll();
339
+ return;
340
+ }
341
+
342
+ if (this.anchor) {
343
+ if (this.isReplaceKey(data)) {
344
+ const replaced = this.replaceSelection(data);
345
+ if (replaced) return;
346
+ // Selection collapsed to empty (anchor met cursor): native key behavior.
347
+ this.anchor = null;
348
+ native(data);
349
+ return;
350
+ }
351
+ // Movement, enter, history, kill/yank, app shortcuts: collapse first, then native behavior.
352
+ this.anchor = null;
353
+ }
354
+
355
+ native(data);
356
+ }
357
+
358
+ /** Let Pi normalize/register paste and take its one undo snapshot, then remove
359
+ * the selected text around the inserted text or marker in that transaction. */
360
+ private replaceSelectionWithPaste(frame: string, native: (data: string) => void): void {
361
+ const range = this.range();
362
+ this.anchor = null;
363
+ if (!range || frame === PASTE_START + PASTE_END) return native(frame);
364
+ const lines = this.s.lines;
365
+ const before = lines.join("\n");
366
+ const offset = (p: Point): number => lines.slice(0, p.line).reduce((n, line) => n + line.length + 1, 0) + p.col;
367
+ const [start, end] = range.map(offset);
368
+ if (start === end) return native(frame);
369
+ const cursor = offset(this.cursor());
370
+ native(frame);
371
+ const after = this.s.lines.join("\n");
372
+ const prefix = before.slice(0, cursor);
373
+ const suffix = before.slice(cursor);
374
+ // If Pi did not perform a plain insertion, retain its native result.
375
+ if (!after.startsWith(prefix) || !after.endsWith(suffix)) return;
376
+ const inserted = after.slice(prefix.length, after.length - suffix.length);
377
+ if (!inserted) return;
378
+ const result = before.slice(0, start) + inserted + before.slice(end);
379
+ this.s.lines = result.split("\n");
380
+ const caret = result.slice(0, start + inserted.length).split("\n");
381
+ this.s.cursorLine = caret.length - 1;
382
+ this.internals.setCursorCol(caret.at(-1)!.length);
383
+ this.internals.lastAction = null;
384
+ this.editor.onChange?.(this.editor.getText());
385
+ this.internals.tui.requestRender();
386
+ }
387
+
388
+ /** Keys whose native effect replaces a selection: backspace/delete (and shift variants) or a printable character. */
389
+ private isReplaceKey(data: string): boolean {
390
+ return (
391
+ matchesKey(data, "backspace") ||
392
+ matchesKey(data, "shift+backspace") ||
393
+ matchesKey(data, "delete") ||
394
+ matchesKey(data, "shift+delete") ||
395
+ this.insertsCharacter(data)
396
+ );
397
+ }
398
+
399
+ /**
400
+ * True when the input inserts a text character (Kitty/CSI-u and
401
+ * modify-other-keys aware, plus raw terminal bytes). The raw fallback
402
+ * accepts at most 4 UTF-16 code units with no control bytes; DEL and C1
403
+ * controls must not count as printable (the editor routes them to
404
+ * delete/other actions before its printable fallback, so re-submitting
405
+ * them after a splice would double-edit).
406
+ */
407
+ private insertsCharacter(data: string): boolean {
408
+ if (decodePrintableKey(data) !== undefined) return true;
409
+ if (data.length > 4) return false;
410
+ for (let i = 0; i < data.length; i++) {
411
+ const c = data.charCodeAt(i);
412
+ if (c < 32 || c === 127 || (c >= 0x80 && c <= 0x9f)) return false;
413
+ }
414
+ return true;
415
+ }
416
+
417
+ private selectToLineEdge(toEnd: boolean): void {
418
+ const before = this.cursor();
419
+ if (toEnd) this.internals.moveToLineEnd();
420
+ else this.internals.moveToLineStart();
421
+ this.internals.exitHistoryBrowsing();
422
+ // Repeated or held presses at the edge KEEP the selection: legacy
423
+ // terminals repeat the press byte-identically. Only a zero-width span
424
+ // (cursor landed exactly on the anchor) collapses.
425
+ this.anchor ??= before;
426
+ if (this.anchor.line === this.s.cursorLine && this.anchor.col === this.s.cursorCol) {
427
+ this.anchor = null;
428
+ }
429
+ if (this.internals.autocompleteState) this.internals.updateAutocomplete();
430
+ this.internals.tui.requestRender();
431
+ }
432
+
433
+ /** Chords the engine itself consumes; extension shortcuts are probed first. */
434
+ private isSelectionChord(data: string): boolean {
435
+ return matchesKey(data, "shift+home") || matchesKey(data, "shift+end")
436
+ || matchesKey(data, "alt+e") || matchesKey(data, "alt+a");
437
+ }
438
+
439
+ /** Select the entire editor text (alt+e / alt+a). Cursor moves to the end of the last line. */
440
+ private selectAll(): void {
441
+ const lines = this.s.lines;
442
+ const lastLine = Math.max(0, lines.length - 1);
443
+ this.anchor = { line: 0, col: 0 };
444
+ this.s.cursorLine = lastLine;
445
+ this.internals.setCursorCol((lines[lastLine] ?? "").length);
446
+ this.internals.lastAction = null;
447
+ this.internals.exitHistoryBrowsing();
448
+ if (this.internals.autocompleteState) this.internals.cancelAutocomplete();
449
+ this.internals.tui.requestRender();
450
+ }
451
+
452
+ /**
453
+ * Replace the active selection in ONE atomic edit (delete, or delete +
454
+ * printable character): a single undo snapshot before any mutation, then
455
+ * one splice. Returns false when the span is empty; the caller falls back
456
+ * to native key behavior.
457
+ */
458
+ private replaceSelection(data: string): boolean {
459
+ const range = this.range();
460
+ if (!range) return false;
461
+ const [start, end] = range;
462
+ if (start.line === end.line && start.col === end.col) return false;
463
+ const resolved = decodePrintableKey(data) ?? (this.insertsCharacter(data) ? data : undefined);
464
+ const cp = resolved?.codePointAt(0) ?? 0;
465
+ const char = cp === 127 || (cp >= 0x80 && cp <= 0x9f) ? undefined : resolved;
466
+ const internals = this.internals;
467
+ const lines = internals.state.lines;
468
+ internals.pushUndoSnapshot();
469
+ const merged =
470
+ (lines[start.line] ?? "").slice(0, start.col) + (char ?? "") + (lines[end.line] ?? "").slice(end.col);
471
+ lines.splice(start.line, end.line - start.line + 1, merged);
472
+ this.anchor = null;
473
+ this.setCursor({ line: start.line, col: start.col + (char?.length ?? 0) });
474
+ internals.exitHistoryBrowsing();
475
+ internals.lastAction = null;
476
+ if (internals.autocompleteState) internals.cancelAutocomplete();
477
+ this.editor.onChange?.(this.editor.getText());
478
+ internals.tui.requestRender();
479
+ return true;
480
+ }
481
+
482
+ /**
483
+ * Render post-pass: wrap the selected span of each visible row in reverse
484
+ * video. `inset` is the number of columns the host render adds on EACH side
485
+ * before the editor's own content (0 for plain content rows, 1 for a
486
+ * one-column frame wall); `width` is the width the content was rendered at.
487
+ */
488
+ decorateRows(rows: string[], width: number, inset: number): string[] {
489
+ const range = this.range();
490
+ if (!range) return rows;
491
+ const internals = this.internals;
492
+ const [start, end] = range;
493
+ const contentWidth = Math.max(1, width - inset * 2 - internals.paddingX * 2);
494
+ const layoutWidth = internals.lastWidth || Math.max(1, contentWidth - (internals.paddingX ? 0 : 1));
495
+ const visual = internals.buildVisualLineMap(layoutWidth);
496
+ for (let r = 0; r < internals.renderedVisibleLineCount; r++) {
497
+ const vr = visual[internals.scrollOffset + r];
498
+ if (!vr || vr.logicalLine < start.line || vr.logicalLine > end.line) continue;
499
+ const from =
500
+ clamp(vr.logicalLine === start.line ? start.col - vr.startCol : 0, 0, vr.length) +
501
+ internals.paddingX +
502
+ inset;
503
+ const to =
504
+ clamp(vr.logicalLine === end.line ? end.col - vr.startCol : vr.length, 0, vr.length) +
505
+ internals.paddingX +
506
+ inset;
507
+ if (to <= from) continue;
508
+ const index = 1 + r; // rows[0] is the top border/rule in every layout
509
+ if (index < rows.length) rows[index] = withReverseSpan(rows[index] ?? "", from, to);
510
+ }
511
+ return rows;
512
+ }
513
+
514
+ /**
515
+ * Bottom-rule selection hint. `corner` re-attaches the host frame's right
516
+ * corner glyph after the label. Widths always add up: the rule is truncated
517
+ * to make exact room for label + corner.
518
+ */
519
+ decorateBottomRule(baseRow: string, width: number, corner: string): string {
520
+ const n = this.selectionLength();
521
+ if (n <= 0) return baseRow;
522
+ const label = ` ${n} char${n === 1 ? "" : "s"} selected - Del deletes - Alt+a select all `;
523
+ const labelWidth = visibleWidth(label);
524
+ if (labelWidth + visibleWidth(corner) + 1 >= width) return baseRow;
525
+ return `${truncateToWidth(baseRow, width - labelWidth - visibleWidth(corner), "")}${label}${corner}`;
526
+ }
527
+ }
@@ -5,7 +5,7 @@ import { dirname, relative, resolve } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktree-registry.ts";
7
7
  import { SessionChanges, readChangeSnapshot, sameSnapshot, textSnapshot, isSessionChangeEvidence,
8
- SESSION_CHANGE_ENTRY, SESSION_CHANGE_EVENT, SESSION_CHANGE_RELAY, type SessionChangeEvidence, type ChangeSnapshot } from "./session-changes.ts";
8
+ SESSION_CHANGE_ENTRY, SESSION_CHANGE_RELAY, type SessionChangeEvidence, type ChangeSnapshot } from "./session-changes.ts";
9
9
 
10
10
  const foreignPublishers = new WeakMap<ExtensionAPI, (sessionId: string, evidence: SessionChangeEvidence) => void>();
11
11
  /** Internal authenticated handoff from a successful child tool callback, never a model message or public event. */
@@ -17,7 +17,10 @@ interface Pending { sessionId: string; inputPath: string; path: string; root: st
17
17
  const normalized = (text: string) => text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
18
18
  const unknown = (): ChangeSnapshot => ({ kind: "unavailable", reason: "Tool snapshots could not be verified; diff unavailable." });
19
19
 
20
- /** Observe explicit write/edit outcomes, never infer ownership from Git status or shell text. */
20
+ /**
21
+ * Observe explicit write/edit outcomes, never infer ownership from Git status or shell text.
22
+ * The evidence feeds session statistics and names the roots /gentle:changes reads from Git.
23
+ */
21
24
  export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env, resolver: WorktreeResolver = resolveSessionWorktree): void {
22
25
  const child = env.GENTLE_PI_AGENTS_CHILD === "1";
23
26
  const pending = new Map<string, Pending>();
@@ -26,8 +29,6 @@ export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.Proces
26
29
  const publish = (evidence: SessionChangeEvidence) => {
27
30
  if (!current || !store || current.sessionManager.getSessionId() !== store.sessionId) return;
28
31
  if (store.record(evidence)) pi.appendEntry(SESSION_CHANGE_ENTRY, { sessionId: store.sessionId, evidence });
29
- const notice = store.takeNotice();
30
- pi.events.emit(SESSION_CHANGE_EVENT, { sessionId: store.sessionId, ...(notice ? { notice } : {}) });
31
32
  };
32
33
  foreignPublishers.set(pi, (sessionId, evidence) => {
33
34
  if (!child && current?.sessionManager.getSessionId() === sessionId && isSessionChangeEvidence(evidence)) publish(evidence);