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
@@ -1,12 +1,14 @@
1
1
  import { constants } from "node:fs";
2
- import { createHash } from "node:crypto";
3
2
  import { open } from "node:fs/promises";
4
3
  import { isAbsolute } from "node:path";
5
4
  import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
6
5
  import { changesModel, type ChangedFile, type WorktreeChanges } from "./shell-changes.ts";
6
+ import { SESSION_WORKTREE_ENTRY } from "./session-worktree-registry.ts";
7
7
 
8
+ // Captured write/edit evidence of a session and its subagents. It feeds
9
+ // session statistics and tells /gentle:changes which roots the session
10
+ // touched; the files Changes lists always come from Git.
8
11
  export const SESSION_CHANGE_ENTRY = "gentle-pi.session-change/v1";
9
- export const SESSION_CHANGE_EVENT = "gentle-pi:session-change";
10
12
  export const SESSION_CHANGE_RELAY = "gentle-pi:child-session-change";
11
13
  export const MAX_CHANGE_BYTES = 64 * 1024;
12
14
  const MAX_RECORDS = 256;
@@ -65,13 +67,29 @@ export function isSessionChangeEvidence(value: unknown): value is SessionChangeE
65
67
  export const sameSnapshot = (a: ChangeSnapshot, b: ChangeSnapshot): boolean =>
66
68
  a.kind === b.kind && (a.kind === "absent" || (a.kind === "text" && b.kind === "text" && a.text === b.text));
67
69
  const snapshotText = (value: ChangeSnapshot) => value.kind === "text" ? value.text : "";
70
+ const shown = (state: FileState) => Boolean(state.unavailable) || !sameSnapshot(state.before, state.after);
71
+
72
+ /**
73
+ * Roots this session works in: its registered worktrees (own root, explicit and
74
+ * subagent registrations) and the roots of captured evidence (subagents in an
75
+ * independent repository). Only roots, never files.
76
+ */
77
+ export function sessionChangeRoots(sessionId: string, entries: readonly Entry[]): string[] {
78
+ const roots = new Set<string>();
79
+ for (const entry of entries) {
80
+ if (entry.type !== "custom" || !entry.data || typeof entry.data !== "object") continue;
81
+ const data = entry.data as { sessionId?: unknown; root?: unknown; evidence?: unknown };
82
+ if (data.sessionId !== sessionId) continue;
83
+ if (entry.customType === SESSION_WORKTREE_ENTRY && typeof data.root === "string" && isAbsolute(data.root)) roots.add(data.root);
84
+ if (entry.customType === SESSION_CHANGE_ENTRY && isSessionChangeEvidence(data.evidence)) roots.add(data.evidence.root);
85
+ }
86
+ return [...roots];
87
+ }
68
88
 
69
89
  export class SessionChanges {
70
90
  private readonly seen = new Set<string>();
71
91
  private readonly files = new Map<string, Map<string, FileState>>();
72
92
  private bytes = 0;
73
- private announced = false;
74
- notice: string | undefined;
75
93
  readonly sessionId: string;
76
94
  private cached: WorktreeChanges[] | undefined;
77
95
  constructor(sessionId: string, entries: readonly Entry[] = []) { this.sessionId = sessionId; this.restore(entries); }
@@ -86,10 +104,8 @@ export class SessionChanges {
86
104
  record(evidence: SessionChangeEvidence): boolean {
87
105
  if (!isSessionChangeEvidence(evidence) || this.seen.has(evidence.id)) return false;
88
106
  const bytes = Buffer.byteLength(JSON.stringify(evidence));
89
- if (this.seen.size >= MAX_RECORDS || this.bytes + bytes > MAX_SESSION_BYTES) {
90
- this.notice = "Session change capture limit reached; additional changes are not displayed.";
91
- return false;
92
- }
107
+ // Bounds session-file growth; Changes itself lists from Git without a cap.
108
+ if (this.seen.size >= MAX_RECORDS || this.bytes + bytes > MAX_SESSION_BYTES) return false;
93
109
  this.seen.add(evidence.id);
94
110
  this.bytes += bytes;
95
111
  if (sameSnapshot(evidence.before, evidence.after)) return false;
@@ -109,13 +125,13 @@ export class SessionChanges {
109
125
  }
110
126
  private changed(root: string): ChangedFile[] {
111
127
  return [...(this.files.get(root) ?? [])].flatMap(([path, state]) => {
112
- if (!state.unavailable && sameSnapshot(state.before, state.after)) return [];
128
+ if (!shown(state)) return [];
113
129
  const patch = this.patch(path, state);
114
130
  const patchLines = patch.split("\n");
115
131
  const firstHunk = patchLines.findIndex(line => line.startsWith("@@"));
116
132
  const lines = firstHunk < 0 ? [] : patchLines.slice(firstHunk + 1);
117
133
  return [{
118
- path, diffRevision: createHash("sha256").update(patch).digest("hex"), status: state.before.kind === "absent" ? "added" as const : state.after.kind === "absent" ? "deleted" as const : "modified" as const,
134
+ path, status: state.before.kind === "absent" ? "added" as const : state.after.kind === "absent" ? "deleted" as const : "modified" as const,
119
135
  added: state.unavailable ? 0 : lines.filter(line => line.startsWith("+")).length,
120
136
  deleted: state.unavailable ? 0 : lines.filter(line => line.startsWith("-")).length,
121
137
  ...(state.unavailable ? { countsUnavailable: state.unavailable } : {}),
@@ -131,17 +147,6 @@ export class SessionChanges {
131
147
  }
132
148
  get model() {
133
149
  const trees = this.worktrees;
134
- return { ...changesModel(trees.flatMap(tree => tree.model.files.map(file => ({ ...file, path: trees.length === 1 ? file.path : tree.root + "/" + file.path })))), ...(this.notice ? { notice: this.notice } : {}) };
135
- }
136
- /** Return the capture-limit notice once, for a single user notification. The `notice` field stays set. */
137
- takeNotice(): string | undefined {
138
- if (!this.notice || this.announced) return undefined;
139
- this.announced = true;
140
- return this.notice;
141
- }
142
- async refresh() { return this.model; }
143
- loadDiff(root: string, file: ChangedFile): string {
144
- const state = this.files.get(root)?.get(file.path);
145
- return state ? this.patch(file.path, state) : "No captured agent diff for this file.";
150
+ return changesModel(trees.flatMap(tree => tree.model.files.map(file => ({ ...file, path: trees.length === 1 ? file.path : tree.root + "/" + file.path }))));
146
151
  }
147
152
  }
@@ -0,0 +1,135 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export const MESSAGING_REASON_MIN_CHARACTERS = 8;
4
+ export const MESSAGING_REASON_MAX_UTF8_BYTES = 512;
5
+
6
+ const PREVIEW_CONTROL_CHARACTER = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
7
+
8
+ function escapePreviewText(value: string): string {
9
+ return Array.from(value, (character) => {
10
+ if (!PREVIEW_CONTROL_CHARACTER.test(character)) return character;
11
+ if (character === "\r") return "\\r";
12
+ if (character === "\n") return "\\n";
13
+ if (character === "\t") return "\\t";
14
+ return `\\u${character.codePointAt(0)!.toString(16).toUpperCase().padStart(4, "0")}`;
15
+ }).join("");
16
+ }
17
+
18
+ export function normalizeMessagingReason(value: unknown): string {
19
+ if (typeof value !== "string") throw new Error("Cross-orchestrator communication requires a concrete reason of at least 8 trimmed characters and at most 512 UTF-8 bytes.");
20
+ const reason = value.trim();
21
+ if (Array.from(reason).length < MESSAGING_REASON_MIN_CHARACTERS || Buffer.byteLength(value, "utf8") > MESSAGING_REASON_MAX_UTF8_BYTES) {
22
+ throw new Error("Cross-orchestrator communication requires a concrete reason of at least 8 trimmed characters and at most 512 UTF-8 bytes.");
23
+ }
24
+ return reason;
25
+ }
26
+
27
+ /**
28
+ * Supported decision tokens for cross-orchestrator communication consent.
29
+ */
30
+ export const MESSAGING_CONSENT_DECISIONS = {
31
+ ALLOW_ONCE: "Allow once",
32
+ ALLOW_SESSION: "Allow for this session",
33
+ DENY: "Deny",
34
+ } as const;
35
+
36
+ export type MessagingConsentDecision =
37
+ (typeof MESSAGING_CONSENT_DECISIONS)[keyof typeof MESSAGING_CONSENT_DECISIONS];
38
+
39
+ /**
40
+ * Options for requesting human authorization before outbound cross-session communication.
41
+ */
42
+ export interface MessagingConsentOptions {
43
+ /** Outbound message payload. */
44
+ message: string;
45
+ /** Concrete caller-supplied reason explaining why this communication is needed. */
46
+ reason: string;
47
+ /** Optional abort signal for request cancellation. */
48
+ signal?: AbortSignal;
49
+ }
50
+
51
+ /**
52
+ * Ephemeral in-memory grant manager for cross-orchestrator communication.
53
+ * Permissions are scoped strictly to the live session identity and session manager instance.
54
+ */
55
+ export class SessionMessagingGrants {
56
+ private readonly grants = new Map<string, Set<string>>();
57
+ private manager?: ExtensionContext["sessionManager"];
58
+ private sessionId?: string;
59
+
60
+ /**
61
+ * Authorizes an outbound cross-orchestrator message to the given recipient.
62
+ * Prompts the user interactively when no session-level grant exists, and fails closed otherwise.
63
+ */
64
+ async authorize(
65
+ ctx: Pick<ExtensionContext, "sessionManager" | "hasUI" | "ui">,
66
+ recipient: string,
67
+ options: MessagingConsentOptions
68
+ ): Promise<void> {
69
+ const message = options.message;
70
+ if (typeof message !== "string" || Buffer.byteLength(message, "utf8") > 8192) throw new Error("Cross-orchestrator message is invalid.");
71
+ const reason = normalizeMessagingReason(options.reason);
72
+ const signal = options.signal;
73
+ if (signal?.aborted) throw new Error("Cross-orchestrator message authorization aborted.");
74
+ const id = ctx.sessionManager.getSessionId();
75
+ if (!id) throw new Error("Cross-orchestrator communication requires an active session identity.");
76
+ if (this.manager !== ctx.sessionManager || this.sessionId !== id) {
77
+ this.grants.clear();
78
+ this.manager = ctx.sessionManager;
79
+ this.sessionId = id;
80
+ }
81
+ if (!ctx.hasUI || typeof ctx.ui?.select !== "function") {
82
+ throw new Error("Cross-orchestrator communication requires interactive human consent before sending.");
83
+ }
84
+ if (this.grants.get(id)?.has(recipient)) return;
85
+
86
+ const previewMessage = escapePreviewText(message);
87
+ const isTruncated = message.length > 200;
88
+ const messageLabel = isTruncated
89
+ ? `Message (${message.length} chars, preview): ${escapePreviewText(message.slice(0, 197))}...`
90
+ : `Message: ${previewMessage}`;
91
+ const title = `Authorize cross-orchestrator message to ${escapePreviewText(recipient)}?\nReason: ${escapePreviewText(reason)}\n${messageLabel}`;
92
+
93
+ const selected = await ctx.ui.select(
94
+ title,
95
+ [
96
+ MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE,
97
+ MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION,
98
+ MESSAGING_CONSENT_DECISIONS.DENY,
99
+ ],
100
+ { signal }
101
+ );
102
+
103
+ if (signal?.aborted) throw new Error("Cross-orchestrator message authorization aborted.");
104
+ if (ctx.sessionManager !== this.manager || ctx.sessionManager.getSessionId() !== id) {
105
+ throw new Error("Session identity changed during authorization.");
106
+ }
107
+
108
+ if (selected === MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION) {
109
+ const sessionGrants = this.grants.get(id) ?? new Set<string>();
110
+ sessionGrants.add(recipient);
111
+ this.grants.set(id, sessionGrants);
112
+ return;
113
+ }
114
+
115
+ if (selected === MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE) {
116
+ return;
117
+ }
118
+
119
+ throw new Error("Cross-orchestrator communication denied by user.");
120
+ }
121
+
122
+ /**
123
+ * Checks whether a live session-level grant exists for the target recipient in the current session.
124
+ */
125
+ assertCurrent(ctx: Pick<ExtensionContext, "sessionManager">, recipient: string): boolean {
126
+ if (
127
+ ctx.sessionManager !== this.manager ||
128
+ !this.sessionId ||
129
+ ctx.sessionManager.getSessionId() !== this.sessionId
130
+ ) {
131
+ return false;
132
+ }
133
+ return this.grants.get(this.sessionId)?.has(recipient) ?? false;
134
+ }
135
+ }
@@ -0,0 +1,83 @@
1
+ // Session-bound active profiles (gentle-shell#1064, slice 1).
2
+ //
3
+ // One parent Pi session may bind one saved profile for its own launches without
4
+ // touching any shared state: no clone pin (`p`), no repository declaration (`P`),
5
+ // no global routing, no materialized stores, no agent frontmatter, no Pi default
6
+ // settings. The binding is an in-process store keyed by the parent session id, so
7
+ // concurrent Pi processes are isolated by construction and a resumed session
8
+ // starts from the shared layers (`p → P → global`) until it binds again.
9
+ // Persistence across resume and reload semantics are deliberate follow-up
10
+ // slices; this module never writes a file.
11
+ //
12
+ // Pi loads every extension entrypoint with its own Jiti instance and
13
+ // `moduleCache: false` (loader.js `loadExtensionModule`), so a module-local Map
14
+ // would be re-evaluated per entrypoint: the panel writes one copy while the
15
+ // launch, usage, and status readers hold their own empty copies
16
+ // (gentle-shell#1558). The store therefore lives on `globalThis` behind a
17
+ // `Symbol.for` key, which every evaluation of this module in one process
18
+ // resolves to the same symbol and therefore the same shared Map.
19
+ import type { AgentModelConfig } from "./model-routing-authority.ts";
20
+
21
+ /** A session's bound profile: its saved name plus the routing snapshot taken at bind time. */
22
+ export interface SessionProfileBinding {
23
+ readonly name: string;
24
+ readonly modelProfiles: AgentModelConfig;
25
+ }
26
+
27
+ const bindingsRegistryKey = Symbol.for("gentle-pi:session-profile-bindings");
28
+
29
+ /** The one process-wide binding store, shared across every extension entrypoint that imports this module. */
30
+ function bindingsBySession(): Map<string, SessionProfileBinding> {
31
+ const holder = globalThis as { [bindingsRegistryKey]?: Map<string, SessionProfileBinding> };
32
+ (holder[bindingsRegistryKey] ??= new Map());
33
+ return holder[bindingsRegistryKey]!;
34
+ }
35
+
36
+ /** Copy one routing snapshot level deep — the layer a defensive copy needs, so a binding never aliases its source or its readers. */
37
+ function cloneProfiles(profiles: AgentModelConfig): AgentModelConfig {
38
+ return Object.fromEntries(Object.entries(profiles).map(([agent, entry]) => [agent, { ...entry }]));
39
+ }
40
+
41
+ /**
42
+ * Bind (or rebind) one parent session to a profile. The routing snapshot is
43
+ * copied, so later edits to the caller's object or to the saved profile
44
+ * definition never mutate a session that already bound it; refreshing a bound
45
+ * session requires an explicit re-selection.
46
+ */
47
+ export function bindSessionProfile(sessionId: string, name: string, modelProfiles: AgentModelConfig): void {
48
+ bindingsBySession().set(sessionId, { name, modelProfiles: cloneProfiles(modelProfiles) });
49
+ }
50
+
51
+ /**
52
+ * Read a session's binding as a private copy. Returns `undefined` for unknown,
53
+ * cleared, or undefined session ids: an unbound session keeps resolving the
54
+ * shared layers exactly as before.
55
+ */
56
+ export function readSessionProfileBinding(sessionId: string | undefined): SessionProfileBinding | undefined {
57
+ if (sessionId === undefined) return undefined;
58
+ const binding = bindingsBySession().get(sessionId);
59
+ return binding === undefined ? undefined : { name: binding.name, modelProfiles: cloneProfiles(binding.modelProfiles) };
60
+ }
61
+
62
+ /** Remove one session's binding without touching any other session's. */
63
+ export function clearSessionProfileBinding(sessionId: string): void {
64
+ bindingsBySession().delete(sessionId);
65
+ }
66
+
67
+ /** Testing-only reset of every binding. */
68
+ export function resetSessionProfileBindingsForTesting(): void {
69
+ bindingsBySession().clear();
70
+ }
71
+
72
+ /**
73
+ * The launch-time precedence for wholesale routing replacement: a session
74
+ * binding outranks a winning pin, a pin alone still replaces, and neither
75
+ * leaves today's routing untouched. Named so the precedence rule a launch and
76
+ * the status display agree on lives in exactly one place.
77
+ */
78
+ export function sessionOrPinModelProfiles(
79
+ session: AgentModelConfig | undefined,
80
+ pin: AgentModelConfig | undefined,
81
+ ): AgentModelConfig | undefined {
82
+ return session ?? pin;
83
+ }
@@ -0,0 +1,266 @@
1
+ // Inherited profile freeze (gentle-shell#1064 slice 3b-ii).
2
+ //
3
+ // A parent session that never pressed Enter in `/gentle:profiles` resolves the
4
+ // shared defaults `p → P → G` (local pin, repository declaration, global
5
+ // `active` in profiles.json) ONCE, at startup, and keeps that snapshot with its
6
+ // origin. Changing a default later only affects new sessions, exactly like Pi's
7
+ // "set as default" for the orchestrator model; an open session changes profile
8
+ // only through its own Enter. Each launch re-resolves the defaults to compare
9
+ // and shows one drift notice per distinct change.
10
+ //
11
+ // Precedence, the single rule every consumer reads through
12
+ // `resolveSessionProfile`:
13
+ //
14
+ // explicit Enter binding → frozen inherited snapshot → live p → P → G
15
+ //
16
+ // The live resolution applies only in `follow` mode (GENTLE_PI_PROFILE_FOLLOW=1,
17
+ // for CI and headless runs), in subagent children (their routing comes from the
18
+ // parent through --model), and when no session id is available. In `follow`
19
+ // mode the orchestrator still comes from the startup profile while children
20
+ // follow later defaults: an accepted, documented limitation.
21
+ //
22
+ // The frozen state is in-process only, like the explicit binding of slice 2: it
23
+ // lives on `globalThis` behind a `Symbol.for` key because Pi evaluates this
24
+ // module once per extension entrypoint. A resumed session (`--resume`,
25
+ // `--continue`, `--session`) therefore freezes again from the current defaults,
26
+ // and its drift memory starts empty. Persisting the freeze as a
27
+ // `gentle-pi.session-profile/v1` entry with origin `local | repo | global` is
28
+ // the integration point with slice 3b-i.
29
+ import { createHash } from "node:crypto";
30
+ import { profileRoleEntries, profilesFilePath, readProfilesFileResult } from "./agent-profiles.ts";
31
+ import { readProfilePinStatus, resolveProfilePin, resolveUnversionedProjectProfile } from "./agent-profile-pin.ts";
32
+ import type { AgentModelConfig } from "./model-routing-authority.ts";
33
+ import { readSessionProfileBinding } from "./session-profile-binding.ts";
34
+ import type { WorktreeIdentity, WorktreeResolver } from "./session-worktree-registry.ts";
35
+
36
+ /** Opt-in that keeps the live `p → P → G` resolution instead of freezing it at startup. */
37
+ export const PROFILE_FOLLOW_ENV = "GENTLE_PI_PROFILE_FOLLOW";
38
+
39
+ /** Which shared default supplied an inherited profile. */
40
+ export type InheritedProfileOrigin = "local" | "repo" | "global";
41
+
42
+ /** A profile resolved from the shared defaults: its name, its origin, and its role routing. */
43
+ export interface InheritedProfile {
44
+ readonly name: string;
45
+ readonly origin: InheritedProfileOrigin;
46
+ readonly modelProfiles: AgentModelConfig;
47
+ }
48
+
49
+ /** A frozen session: `profile` is `undefined` when no layer had a profile at startup. */
50
+ export interface FrozenInheritedProfile {
51
+ readonly profile: InheritedProfile | undefined;
52
+ }
53
+
54
+ /**
55
+ * The profile a session uses. `inherited` is the startup snapshot (with or
56
+ * without a profile); `follow` and `live` carry the current defaults.
57
+ */
58
+ export type SessionProfileResolution =
59
+ | { kind: "explicit"; name: string; modelProfiles: AgentModelConfig }
60
+ | { kind: "inherited"; profile: InheritedProfile | undefined }
61
+ | { kind: "follow"; profile: InheritedProfile | undefined }
62
+ | { kind: "live"; profile: InheritedProfile | undefined };
63
+
64
+ export interface InheritedProfileOptions {
65
+ cwd: string;
66
+ configHome: string;
67
+ resolveWorktree?: WorktreeResolver;
68
+ }
69
+
70
+ export interface SessionProfileOptions extends InheritedProfileOptions {
71
+ sessionId: string | undefined;
72
+ env?: NodeJS.ProcessEnv;
73
+ }
74
+
75
+ interface FreezeRegistry {
76
+ frozen: Map<string, FrozenInheritedProfile>;
77
+ // Drift fingerprints already reported, per session.
78
+ notified: Map<string, Set<string>>;
79
+ }
80
+
81
+ const freezeRegistryKey = Symbol.for("gentle-pi:session-profile-freeze");
82
+
83
+ function registry(): FreezeRegistry {
84
+ const holder = globalThis as { [freezeRegistryKey]?: FreezeRegistry };
85
+ (holder[freezeRegistryKey] ??= { frozen: new Map(), notified: new Map() });
86
+ return holder[freezeRegistryKey]!;
87
+ }
88
+
89
+ function cloneProfiles(profiles: AgentModelConfig): AgentModelConfig {
90
+ return Object.fromEntries(Object.entries(profiles).map(([agent, entry]) => [agent, { ...entry }]));
91
+ }
92
+
93
+ function cloneProfile(profile: InheritedProfile | undefined): InheritedProfile | undefined {
94
+ return profile === undefined ? undefined : { name: profile.name, origin: profile.origin, modelProfiles: cloneProfiles(profile.modelProfiles) };
95
+ }
96
+
97
+ function roleRouting(config: AgentModelConfig): AgentModelConfig {
98
+ return Object.fromEntries(profileRoleEntries(config).map(([agent, entry]) => [agent, { ...entry }]));
99
+ }
100
+
101
+ export function profileFollowEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
102
+ return env[PROFILE_FOLLOW_ENV] === "1";
103
+ }
104
+
105
+ function isChildSession(env: NodeJS.ProcessEnv): boolean {
106
+ return env.GENTLE_PI_AGENTS_CHILD === "1";
107
+ }
108
+
109
+ /**
110
+ * The shared defaults `p → P → G` for one directory, with the layer that won.
111
+ * Pin layers keep `resolveProfilePin` semantics; outside a Git worktree the
112
+ * repository declaration is read as admission does for non-Git projects; the
113
+ * global layer is the `active` profile of profiles.json. The orchestrator key
114
+ * is never part of the routing. `undefined` means no layer has a profile.
115
+ */
116
+ export function resolveInheritedProfile(options: InheritedProfileOptions): InheritedProfile | undefined {
117
+ // One worktree lookup serves both the pin layers and the non-Git check.
118
+ let resolved = false;
119
+ let identity: WorktreeIdentity | undefined;
120
+ const resolveWorktree: WorktreeResolver | undefined = options.resolveWorktree === undefined ? undefined : (cwd, base) => {
121
+ if (!resolved) {
122
+ resolved = true;
123
+ identity = options.resolveWorktree!(cwd, base);
124
+ }
125
+ return identity;
126
+ };
127
+ const pin = resolveProfilePin({ cwd: options.cwd, configHome: options.configHome, resolveWorktree });
128
+ if (pin !== undefined) return { name: pin.profile, origin: pin.source, modelProfiles: pin.modelProfiles };
129
+ if (readProfilePinStatus(options.cwd, resolveWorktree) === undefined) {
130
+ const declaration = resolveUnversionedProjectProfile(options.cwd, options.configHome);
131
+ if (declaration !== undefined) return { name: declaration.profile, origin: "repo", modelProfiles: declaration.modelProfiles };
132
+ }
133
+ const store = readProfilesFileResult(profilesFilePath(options.configHome));
134
+ if (store.status !== "valid" || store.file.active === undefined) return undefined;
135
+ const active = store.file.active;
136
+ if (!Object.prototype.hasOwnProperty.call(store.file.profiles, active)) return undefined;
137
+ return { name: active, origin: "global", modelProfiles: roleRouting(store.file.profiles[active]!) };
138
+ }
139
+
140
+ /** A copy of one session's frozen snapshot, or `undefined` when it never froze. */
141
+ export function readFrozenInheritedProfile(sessionId: string | undefined): FrozenInheritedProfile | undefined {
142
+ if (sessionId === undefined) return undefined;
143
+ const frozen = registry().frozen.get(sessionId);
144
+ return frozen === undefined ? undefined : { profile: cloneProfile(frozen.profile) };
145
+ }
146
+
147
+ function freezes(options: SessionProfileOptions): boolean {
148
+ const env = options.env ?? process.env;
149
+ return options.sessionId !== undefined && options.sessionId.length > 0 && !isChildSession(env) && !profileFollowEnabled(env);
150
+ }
151
+
152
+ // Only called after `freezes(options)`, which guarantees a session id.
153
+ function frozenOrFreeze(options: SessionProfileOptions): FrozenInheritedProfile {
154
+ const store = registry().frozen;
155
+ const sessionId = options.sessionId!;
156
+ let frozen = store.get(sessionId);
157
+ if (frozen === undefined) {
158
+ frozen = { profile: cloneProfile(resolveInheritedProfile(options)) };
159
+ store.set(sessionId, frozen);
160
+ }
161
+ return { profile: cloneProfile(frozen.profile) };
162
+ }
163
+
164
+ /**
165
+ * Freeze a parent session's inherited profile at startup. Idempotent: the
166
+ * first snapshot wins, so a session keeps it across `/reload` in the same
167
+ * process. Children and `follow` sessions never freeze.
168
+ */
169
+ export function freezeSessionProfileAtStartup(options: SessionProfileOptions): void {
170
+ if (freezes(options)) frozenOrFreeze(options);
171
+ }
172
+
173
+ /**
174
+ * The single precedence rule for launch, status and Usage. A parent session
175
+ * that has not frozen yet freezes here, so the snapshot never depends on which
176
+ * extension's `session_start` ran first.
177
+ */
178
+ export function resolveSessionProfile(options: SessionProfileOptions): SessionProfileResolution {
179
+ const explicit = readSessionProfileBinding(options.sessionId);
180
+ if (explicit !== undefined) return { kind: "explicit", name: explicit.name, modelProfiles: explicit.modelProfiles };
181
+ if (freezes(options)) return { kind: "inherited", profile: frozenOrFreeze(options).profile };
182
+ const env = options.env ?? process.env;
183
+ const profile = resolveInheritedProfile(options);
184
+ return isChildSession(env) || !profileFollowEnabled(env) ? { kind: "live", profile } : { kind: "follow", profile };
185
+ }
186
+
187
+ /** The routing that replaces subagent model profiles, or `undefined` for today's routing. */
188
+ export function sessionProfileModelProfiles(resolution: SessionProfileResolution): AgentModelConfig | undefined {
189
+ return resolution.kind === "explicit" ? resolution.modelProfiles : resolution.profile?.modelProfiles;
190
+ }
191
+
192
+ /**
193
+ * The routing one launch target uses under a resolution read once per request.
194
+ * Explicit and frozen profiles belong to the session, so they apply wherever
195
+ * the launch goes, a foreign repository included; a `follow` or `live`
196
+ * resolution reads the defaults of the target itself. A session frozen without
197
+ * a profile has nothing to carry into a foreign repository, so that
198
+ * repository keeps its own pin layers (local pin, then declaration), as
199
+ * before the freeze; the global active profile is not the repository's own
200
+ * default and does not apply there either.
201
+ */
202
+ export function sessionProfileRoutingAt(
203
+ resolution: SessionProfileResolution,
204
+ target: InheritedProfileOptions & { foreignRepository?: boolean },
205
+ ): AgentModelConfig | undefined {
206
+ if (resolution.kind === "explicit") return resolution.modelProfiles;
207
+ if (resolution.kind === "inherited") {
208
+ if (resolution.profile !== undefined || target.foreignRepository !== true) return resolution.profile?.modelProfiles;
209
+ return resolveProfilePin(target)?.modelProfiles;
210
+ }
211
+ return resolveInheritedProfile(target)?.modelProfiles;
212
+ }
213
+
214
+ /** The status label, spelled as before: `name (session)`, `name (local)`, `name (repo)`, or the bare global name. */
215
+ export function sessionProfileLabel(resolution: SessionProfileResolution): string | undefined {
216
+ if (resolution.kind === "explicit") return `${resolution.name} (session)`;
217
+ const profile = resolution.profile;
218
+ if (profile === undefined) return undefined;
219
+ return profile.origin === "global" ? profile.name : `${profile.name} (${profile.origin})`;
220
+ }
221
+
222
+ function stableJson(value: unknown): string {
223
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
224
+ if (value !== null && typeof value === "object") {
225
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson((value as Record<string, unknown>)[key])}`).join(",")}}`;
226
+ }
227
+ return JSON.stringify(value) ?? "null";
228
+ }
229
+
230
+ // The origin does not route anything: the same profile reached through another
231
+ // layer is not drift.
232
+ function fingerprint(profile: InheritedProfile | undefined): string {
233
+ if (profile === undefined) return "none";
234
+ return createHash("sha256").update(stableJson({ name: profile.name, modelProfiles: profile.modelProfiles })).digest("hex");
235
+ }
236
+
237
+ function describe(profile: InheritedProfile | undefined): string {
238
+ return profile === undefined ? "no profile" : `"${profile.name}" (${profile.origin})`;
239
+ }
240
+
241
+ /**
242
+ * Compare a frozen inherited session with the current defaults. Returns the
243
+ * one-line notice the first time a given current default differs from the
244
+ * snapshot, and `undefined` otherwise: the same change is never reported
245
+ * twice in one session. Explicit bindings, `follow` sessions and children are
246
+ * never compared. The comparison never adopts the new default.
247
+ */
248
+ export function inheritedProfileDriftNotice(options: SessionProfileOptions): string | undefined {
249
+ const resolution = resolveSessionProfile(options);
250
+ if (resolution.kind !== "inherited") return undefined;
251
+ const current = resolveInheritedProfile(options);
252
+ const key = fingerprint(current);
253
+ if (key === fingerprint(resolution.profile)) return undefined;
254
+ const notified = registry().notified;
255
+ const seen = notified.get(options.sessionId!) ?? new Set<string>();
256
+ if (seen.has(key)) return undefined;
257
+ seen.add(key);
258
+ notified.set(options.sessionId!, seen);
259
+ return `el Gentleman: the default profile changed to ${describe(current)}, this session keeps ${describe(resolution.profile)}. Press Enter on a profile in /gentle:profiles to adopt it.`;
260
+ }
261
+
262
+ /** Testing-only reset of every frozen snapshot and drift memory. */
263
+ export function resetFrozenInheritedProfilesForTesting(): void {
264
+ registry().frozen.clear();
265
+ registry().notified.clear();
266
+ }