gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,163 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { AgentModelConfig } from "../lib/model-routing-authority.ts";
4
+ import {
5
+ bindSessionProfile,
6
+ clearSessionProfileBinding,
7
+ readSessionProfileBinding,
8
+ resetSessionProfileBindingsForTesting,
9
+ sessionOrPinModelProfiles,
10
+ } from "../lib/session-profile-binding.ts";
11
+
12
+ const ROUTING: AgentModelConfig = {
13
+ worker: { model: "zai/glm-4.7", thinking: "medium" },
14
+ reviewer: { model: "openai/o4-mini" },
15
+ };
16
+
17
+ test("bind and read roundtrip the snapshot by session id", () => {
18
+ resetSessionProfileBindingsForTesting();
19
+ bindSessionProfile("session-a", "work", ROUTING);
20
+ const binding = readSessionProfileBinding("session-a");
21
+ assert.equal(binding?.name, "work");
22
+ assert.equal(binding?.modelProfiles.worker?.model, "zai/glm-4.7");
23
+ assert.equal(binding?.modelProfiles.worker?.thinking, "medium");
24
+ });
25
+
26
+ test("bindings are isolated per session id", () => {
27
+ resetSessionProfileBindingsForTesting();
28
+ bindSessionProfile("session-a", "work", ROUTING);
29
+ bindSessionProfile("session-b", "personal", { solo: { model: "openai/o4-mini" } });
30
+ assert.equal(readSessionProfileBinding("session-a")?.name, "work");
31
+ assert.equal(readSessionProfileBinding("session-b")?.name, "personal");
32
+ assert.equal(readSessionProfileBinding("session-c"), undefined);
33
+ });
34
+
35
+ test("rebinding one session replaces its binding without touching others", () => {
36
+ resetSessionProfileBindingsForTesting();
37
+ bindSessionProfile("session-a", "work", ROUTING);
38
+ bindSessionProfile("session-b", "personal", ROUTING);
39
+ bindSessionProfile("session-a", "review", { auditor: { model: "zai/glm-4.7" } });
40
+ assert.equal(readSessionProfileBinding("session-a")?.name, "review");
41
+ assert.equal(readSessionProfileBinding("session-b")?.name, "personal");
42
+ });
43
+
44
+ test("stored snapshots are immune to later mutation of the source object", () => {
45
+ resetSessionProfileBindingsForTesting();
46
+ const source: AgentModelConfig = { worker: { model: "zai/glm-4.7" } };
47
+ bindSessionProfile("session-a", "work", source);
48
+ source.worker!.model = "openai/o4-mini";
49
+ assert.equal(readSessionProfileBinding("session-a")?.modelProfiles.worker?.model, "zai/glm-4.7");
50
+ });
51
+
52
+ test("read snapshots are copies: callers cannot corrupt the store", () => {
53
+ resetSessionProfileBindingsForTesting();
54
+ bindSessionProfile("session-a", "work", ROUTING);
55
+ const first = readSessionProfileBinding("session-a");
56
+ first!.modelProfiles.worker!.model = "mutated/evil";
57
+ const second = readSessionProfileBinding("session-a");
58
+ assert.equal(second?.modelProfiles.worker?.model, "zai/glm-4.7");
59
+ });
60
+
61
+ test("readSessionProfileBinding(undefined) never resolves a binding", () => {
62
+ resetSessionProfileBindingsForTesting();
63
+ bindSessionProfile("session-a", "work", ROUTING);
64
+ assert.equal(readSessionProfileBinding(undefined), undefined);
65
+ });
66
+
67
+ test("clearSessionProfileBinding removes only the named session", () => {
68
+ resetSessionProfileBindingsForTesting();
69
+ bindSessionProfile("session-a", "work", ROUTING);
70
+ bindSessionProfile("session-b", "personal", ROUTING);
71
+ clearSessionProfileBinding("session-a");
72
+ assert.equal(readSessionProfileBinding("session-a"), undefined);
73
+ assert.equal(readSessionProfileBinding("session-b")?.name, "personal");
74
+ });
75
+
76
+ test("sessionOrPinModelProfiles keeps the wholesale-replacement contract: session wins, pin alone, else undefined", () => {
77
+ resetSessionProfileBindingsForTesting();
78
+ const session: AgentModelConfig = { worker: { model: "zai/glm-4.7" } };
79
+ const pin: AgentModelConfig = { worker: { model: "openai/o4-mini" } };
80
+ assert.equal(sessionOrPinModelProfiles(session, pin), session);
81
+ assert.equal(sessionOrPinModelProfiles(undefined, pin), pin);
82
+ assert.equal(sessionOrPinModelProfiles(session, undefined), session);
83
+ assert.equal(sessionOrPinModelProfiles(undefined, undefined), undefined);
84
+ });
85
+
86
+ test("session bindings from one process never leak into a fresh map state", () => {
87
+ resetSessionProfileBindingsForTesting();
88
+ bindSessionProfile("session-a", "work", ROUTING);
89
+ resetSessionProfileBindingsForTesting();
90
+ assert.equal(readSessionProfileBinding("session-a"), undefined);
91
+ });
92
+
93
+ // Cross-entrypoint sharing of the store (gentle-shell#1558).
94
+ //
95
+ // Pi loads every extension entrypoint with its own Jiti instance and
96
+ // `moduleCache: false`, so each entrypoint re-evaluates `lib/` modules and a
97
+ // module-local store would give the panel (gentle-ai.ts) one Map while the
98
+ // launch, usage, and status readers (gentle-agents.ts, gentle-shell.ts) hold
99
+ // their own empty copies. These tests reproduce that isolation the same way
100
+ // the loader creates it: a second import of the module under a distinct URL is
101
+ // a separate module record in the same process, standing in for a second
102
+ // extension entrypoint.
103
+ //
104
+ // The specifier is built as a URL string because the TypeScript checker
105
+ // cannot resolve a literal query-string import; at runtime Node still keys
106
+ // the module cache on the full URL, giving this evaluation its own copy.
107
+ const secondEntrypointSpecifier = new URL("../lib/session-profile-binding.ts?gentle-agents-entrypoint", import.meta.url).href;
108
+ const otherEntrypoint = await import(secondEntrypointSpecifier);
109
+
110
+ test("a binding written by one entrypoint is visible to every other entrypoint", (t) => {
111
+ resetSessionProfileBindingsForTesting();
112
+ otherEntrypoint.resetSessionProfileBindingsForTesting();
113
+ t.after(() => {
114
+ resetSessionProfileBindingsForTesting();
115
+ otherEntrypoint.resetSessionProfileBindingsForTesting();
116
+ });
117
+ bindSessionProfile("session-1", "work", { worker: { model: "zai/glm-4.7" } });
118
+ const read = otherEntrypoint.readSessionProfileBinding("session-1");
119
+ assert.equal(read?.name, "work", "the launch-path entrypoint must see what the panel wrote");
120
+ assert.equal(read?.modelProfiles.worker?.model, "zai/glm-4.7", "the routing snapshot crosses entrypoints intact");
121
+ resetSessionProfileBindingsForTesting();
122
+ });
123
+
124
+ test("a binding written by a non-panel entrypoint is visible to the panel entrypoint", (t) => {
125
+ resetSessionProfileBindingsForTesting();
126
+ t.after(() => {
127
+ resetSessionProfileBindingsForTesting();
128
+ otherEntrypoint.resetSessionProfileBindingsForTesting();
129
+ });
130
+ otherEntrypoint.bindSessionProfile("session-2", "beta", { explore: { model: "openai-codex/gpt-5.6-terra" } });
131
+ assert.equal(readSessionProfileBinding("session-2")?.name, "beta");
132
+ resetSessionProfileBindingsForTesting();
133
+ });
134
+
135
+ test("clearing from one entrypoint unbinds every entrypoint", (t) => {
136
+ resetSessionProfileBindingsForTesting();
137
+ t.after(() => {
138
+ resetSessionProfileBindingsForTesting();
139
+ otherEntrypoint.resetSessionProfileBindingsForTesting();
140
+ });
141
+ bindSessionProfile("session-3", "work", { worker: { model: "zai/glm-4.7" } });
142
+ otherEntrypoint.clearSessionProfileBinding("session-3");
143
+ assert.equal(readSessionProfileBinding("session-3"), undefined, "a cleared binding falls back in every entrypoint");
144
+ assert.equal(otherEntrypoint.readSessionProfileBinding("session-3"), undefined);
145
+ resetSessionProfileBindingsForTesting();
146
+ });
147
+
148
+ test("each entrypoint still hands out private copies of the shared store", (t) => {
149
+ resetSessionProfileBindingsForTesting();
150
+ t.after(() => {
151
+ resetSessionProfileBindingsForTesting();
152
+ otherEntrypoint.resetSessionProfileBindingsForTesting();
153
+ });
154
+ bindSessionProfile("session-4", "work", { worker: { model: "zai/glm-4.7" } });
155
+ const first = otherEntrypoint.readSessionProfileBinding("session-4");
156
+ first!.modelProfiles.worker!.model = "mutated/elsewhere";
157
+ assert.equal(
158
+ otherEntrypoint.readSessionProfileBinding("session-4")?.modelProfiles.worker?.model,
159
+ "zai/glm-4.7",
160
+ "mutating one reader's copy never reaches another reader",
161
+ );
162
+ resetSessionProfileBindingsForTesting();
163
+ });
@@ -0,0 +1,394 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test, { after, beforeEach } from "node:test";
6
+ import { createProfile, emptyProfilesFile, profilesFilePath, setActiveProfile, writeProfilesFileSync } from "../lib/agent-profiles.ts";
7
+ import { clearProfilePinSync, localProfilePinPath, repoProfileDeclarationPath, setProfilePinWorktreeResolverForTesting, writeProfilePinSync } from "../lib/agent-profile-pin.ts";
8
+ import type { AgentModelConfig } from "../lib/model-routing-authority.ts";
9
+ import { bindSessionProfile, resetSessionProfileBindingsForTesting } from "../lib/session-profile-binding.ts";
10
+ import {
11
+ PROFILE_FOLLOW_ENV,
12
+ freezeSessionProfileAtStartup,
13
+ inheritedProfileDriftNotice,
14
+ profileFollowEnabled,
15
+ sessionProfileRoutingAt,
16
+ readFrozenInheritedProfile,
17
+ resetFrozenInheritedProfilesForTesting,
18
+ resolveInheritedProfile,
19
+ resolveSessionProfile,
20
+ sessionProfileLabel,
21
+ sessionProfileModelProfiles,
22
+ } from "../lib/session-profile-freeze.ts";
23
+ import type { WorktreeIdentity, WorktreeResolver } from "../lib/session-worktree-registry.ts";
24
+
25
+ // Inherited profile freeze (gentle-shell#1064 slice 3b-ii). A parent session that
26
+ // never pressed Enter resolves `p → P → G` once and keeps that snapshot; `follow`
27
+ // keeps the live resolution; children never freeze. The worktree identity is
28
+ // injected, so these tests never run Git.
29
+
30
+ const root = mkdtempSync(join(tmpdir(), "gentle-pi-profile-freeze-"));
31
+ after(() => rmSync(root, { recursive: true, force: true }));
32
+
33
+ const FRONTIER: AgentModelConfig = { worker: { model: "anthropic/claude-opus", thinking: "high" } };
34
+ const LOCAL: AgentModelConfig = { worker: { model: "ollama/qwen", thinking: "low" } };
35
+ const TEAM: AgentModelConfig = { worker: { model: "openai/gpt-5" }, orchestrator: { model: "openai/gpt-5" } };
36
+
37
+ let caseIndex = 0;
38
+ interface Fixture {
39
+ configHome: string;
40
+ identity: WorktreeIdentity;
41
+ resolveWorktree: WorktreeResolver;
42
+ cwd: string;
43
+ setActive(name: string | undefined): void;
44
+ pinLocal(name: string | undefined): void;
45
+ declareRepo(name: string | undefined): void;
46
+ editProfile(name: string, config: AgentModelConfig): void;
47
+ }
48
+
49
+ function fixture(profiles: Record<string, AgentModelConfig> = { frontier: FRONTIER, local: LOCAL, team: TEAM }): Fixture {
50
+ const base = join(root, `case-${caseIndex++}`);
51
+ const configHome = join(base, "config");
52
+ const identity = { root: join(base, "worktree"), commonDir: join(base, "git-common") };
53
+ mkdirSync(identity.root, { recursive: true });
54
+ let current = profiles;
55
+ let active: string | undefined;
56
+ const write = () => {
57
+ let file = Object.entries(current).reduce((acc, [name, config]) => createProfile(acc, name, config), emptyProfilesFile());
58
+ if (active !== undefined) file = setActiveProfile(file, active);
59
+ writeProfilesFileSync(profilesFilePath(configHome), file);
60
+ };
61
+ write();
62
+ return {
63
+ configHome,
64
+ identity,
65
+ resolveWorktree: () => identity,
66
+ cwd: identity.root,
67
+ setActive(name) { active = name; write(); },
68
+ pinLocal(name) {
69
+ const path = localProfilePinPath(identity.commonDir);
70
+ if (name === undefined) clearProfilePinSync(path); else writeProfilePinSync(path, name);
71
+ },
72
+ declareRepo(name) {
73
+ const path = repoProfileDeclarationPath(identity.root);
74
+ if (name === undefined) clearProfilePinSync(path); else writeProfilePinSync(path, name);
75
+ },
76
+ editProfile(name, config) { current = { ...current, [name]: config }; write(); },
77
+ };
78
+ }
79
+
80
+ function options(f: Fixture, sessionId: string | undefined, env: NodeJS.ProcessEnv = {}) {
81
+ return { sessionId, cwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, env };
82
+ }
83
+
84
+ beforeEach(() => {
85
+ resetFrozenInheritedProfilesForTesting();
86
+ resetSessionProfileBindingsForTesting();
87
+ });
88
+
89
+ test("resolveInheritedProfile reports the local pin with its origin", () => {
90
+ const f = fixture();
91
+ f.pinLocal("frontier");
92
+ f.declareRepo("team");
93
+ f.setActive("local");
94
+ assert.deepEqual(resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree }), {
95
+ name: "frontier",
96
+ origin: "local",
97
+ modelProfiles: FRONTIER,
98
+ });
99
+ });
100
+
101
+ test("resolveInheritedProfile falls to the repository declaration, without the orchestrator key", () => {
102
+ const f = fixture();
103
+ f.declareRepo("team");
104
+ f.setActive("local");
105
+ const profile = resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree });
106
+ assert.deepEqual(JSON.parse(JSON.stringify(profile)), {
107
+ name: "team",
108
+ origin: "repo",
109
+ modelProfiles: { worker: { model: "openai/gpt-5" } },
110
+ });
111
+ });
112
+
113
+ test("resolveInheritedProfile falls to the global active profile in profiles.json", () => {
114
+ const f = fixture();
115
+ f.setActive("local");
116
+ assert.deepEqual(resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree }), {
117
+ name: "local",
118
+ origin: "global",
119
+ modelProfiles: LOCAL,
120
+ });
121
+ });
122
+
123
+ test("resolveInheritedProfile asks the default worktree resolver once when no resolver is passed", (t) => {
124
+ const f = fixture();
125
+ f.setActive("local");
126
+ let lookups = 0;
127
+ setProfilePinWorktreeResolverForTesting(() => { lookups++; return f.identity; });
128
+ t.after(() => setProfilePinWorktreeResolverForTesting());
129
+ assert.equal(resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome })?.name, "local");
130
+ assert.equal(lookups, 1, "the no-pin path reuses one worktree lookup");
131
+ f.pinLocal("frontier");
132
+ assert.equal(resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome })?.origin, "local");
133
+ assert.equal(lookups, 2);
134
+ });
135
+
136
+ test("resolveInheritedProfile returns undefined when no layer has a profile", () => {
137
+ const f = fixture();
138
+ assert.equal(resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree }), undefined);
139
+ });
140
+
141
+ test("resolveInheritedProfile reads the repository declaration of a non-Git project", () => {
142
+ const f = fixture();
143
+ f.declareRepo("team");
144
+ f.setActive("local");
145
+ const profile = resolveInheritedProfile({ cwd: f.cwd, configHome: f.configHome, resolveWorktree: () => undefined });
146
+ assert.equal(profile?.name, "team");
147
+ assert.equal(profile?.origin, "repo");
148
+ });
149
+
150
+ test("a session freezes the inherited profile once: later pin and active changes do not move it", () => {
151
+ const f = fixture();
152
+ f.pinLocal("frontier");
153
+ const first = resolveSessionProfile(options(f, "session-a"));
154
+ assert.equal(first.kind, "inherited");
155
+ f.pinLocal("local");
156
+ f.setActive("team");
157
+ const second = resolveSessionProfile(options(f, "session-a"));
158
+ assert.deepEqual(second, { kind: "inherited", profile: { name: "frontier", origin: "local", modelProfiles: FRONTIER } });
159
+ assert.deepEqual(sessionProfileModelProfiles(second), FRONTIER);
160
+ });
161
+
162
+ test("editing the frozen profile in profiles.json does not change the frozen content", () => {
163
+ const f = fixture();
164
+ f.setActive("frontier");
165
+ freezeSessionProfileAtStartup(options(f, "session-a"));
166
+ f.editProfile("frontier", LOCAL);
167
+ assert.deepEqual(sessionProfileModelProfiles(resolveSessionProfile(options(f, "session-a"))), FRONTIER);
168
+ });
169
+
170
+ test("a session started after the change freezes the new default; the open one keeps its own", () => {
171
+ const f = fixture();
172
+ f.pinLocal("frontier");
173
+ freezeSessionProfileAtStartup(options(f, "session-a"));
174
+ f.pinLocal("local");
175
+ freezeSessionProfileAtStartup(options(f, "session-b"));
176
+ assert.equal(readFrozenInheritedProfile("session-a")?.profile?.name, "frontier");
177
+ assert.equal(readFrozenInheritedProfile("session-b")?.profile?.name, "local");
178
+ });
179
+
180
+ test("startup freeze is idempotent: a second call keeps the first snapshot", () => {
181
+ const f = fixture();
182
+ f.setActive("frontier");
183
+ freezeSessionProfileAtStartup(options(f, "session-a"));
184
+ f.setActive("local");
185
+ freezeSessionProfileAtStartup(options(f, "session-a"));
186
+ assert.equal(readFrozenInheritedProfile("session-a")?.profile?.name, "frontier");
187
+ });
188
+
189
+ test("a session with no profile anywhere freezes 'no profile' and keeps today's routing", () => {
190
+ const f = fixture();
191
+ const first = resolveSessionProfile(options(f, "session-a"));
192
+ assert.deepEqual(first, { kind: "inherited", profile: undefined });
193
+ assert.equal(sessionProfileModelProfiles(first), undefined);
194
+ f.pinLocal("frontier");
195
+ const second = resolveSessionProfile(options(f, "session-a"));
196
+ assert.deepEqual(second, { kind: "inherited", profile: undefined });
197
+ assert.equal(sessionProfileModelProfiles(second), undefined);
198
+ assert.equal(sessionProfileLabel(second), undefined);
199
+ });
200
+
201
+ test("an explicit Enter binding outranks the frozen inherited profile", () => {
202
+ const f = fixture();
203
+ f.pinLocal("frontier");
204
+ freezeSessionProfileAtStartup(options(f, "session-a"));
205
+ bindSessionProfile("session-a", "team", { worker: { model: "openai/gpt-5" } });
206
+ const resolution = resolveSessionProfile(options(f, "session-a"));
207
+ assert.deepEqual(resolution, { kind: "explicit", name: "team", modelProfiles: { worker: { model: "openai/gpt-5" } } });
208
+ assert.equal(sessionProfileLabel(resolution), "team (session)");
209
+ });
210
+
211
+ test("follow mode re-resolves p → P → G on every call and never freezes", () => {
212
+ const f = fixture();
213
+ const env = { [PROFILE_FOLLOW_ENV]: "1" };
214
+ f.pinLocal("frontier");
215
+ freezeSessionProfileAtStartup(options(f, "session-a", env));
216
+ assert.equal(readFrozenInheritedProfile("session-a"), undefined);
217
+ assert.equal(resolveSessionProfile(options(f, "session-a", env)).kind, "follow");
218
+ assert.deepEqual(sessionProfileModelProfiles(resolveSessionProfile(options(f, "session-a", env))), FRONTIER);
219
+ f.pinLocal("local");
220
+ assert.deepEqual(sessionProfileModelProfiles(resolveSessionProfile(options(f, "session-a", env))), LOCAL);
221
+ assert.equal(readFrozenInheritedProfile("session-a"), undefined);
222
+ });
223
+
224
+ test("Enter in follow mode makes the binding explicit: later defaults no longer apply", () => {
225
+ const f = fixture();
226
+ const env = { [PROFILE_FOLLOW_ENV]: "1" };
227
+ f.pinLocal("frontier");
228
+ bindSessionProfile("session-a", "team", { worker: { model: "openai/gpt-5" } });
229
+ f.pinLocal("local");
230
+ const resolution = resolveSessionProfile(options(f, "session-a", env));
231
+ assert.equal(resolution.kind, "explicit");
232
+ assert.deepEqual(sessionProfileModelProfiles(resolution), { worker: { model: "openai/gpt-5" } });
233
+ });
234
+
235
+ test("profileFollowEnabled accepts only GENTLE_PI_PROFILE_FOLLOW=1", () => {
236
+ assert.equal(PROFILE_FOLLOW_ENV, "GENTLE_PI_PROFILE_FOLLOW");
237
+ assert.equal(profileFollowEnabled({ GENTLE_PI_PROFILE_FOLLOW: "1" }), true);
238
+ assert.equal(profileFollowEnabled({ GENTLE_PI_PROFILE_FOLLOW: "0" }), false);
239
+ assert.equal(profileFollowEnabled({ GENTLE_PI_PROFILE_FOLLOW: "" }), false);
240
+ assert.equal(profileFollowEnabled({}), false);
241
+ });
242
+
243
+ test("subagent children never freeze: they resolve live", () => {
244
+ const f = fixture();
245
+ const env = { GENTLE_PI_AGENTS_CHILD: "1" };
246
+ f.pinLocal("frontier");
247
+ freezeSessionProfileAtStartup(options(f, "child-a", env));
248
+ assert.equal(readFrozenInheritedProfile("child-a"), undefined);
249
+ const resolution = resolveSessionProfile(options(f, "child-a", env));
250
+ assert.equal(resolution.kind, "live");
251
+ assert.equal(readFrozenInheritedProfile("child-a"), undefined);
252
+ f.pinLocal("local");
253
+ assert.deepEqual(sessionProfileModelProfiles(resolveSessionProfile(options(f, "child-a", env))), LOCAL);
254
+ });
255
+
256
+ test("without a session id the resolution is live and nothing is frozen", () => {
257
+ const f = fixture();
258
+ f.pinLocal("frontier");
259
+ const resolution = resolveSessionProfile(options(f, undefined));
260
+ assert.equal(resolution.kind, "live");
261
+ assert.deepEqual(sessionProfileModelProfiles(resolution), FRONTIER);
262
+ });
263
+
264
+ test("labels keep today's spelling: (session), (local), (repo), bare global name", () => {
265
+ const f = fixture();
266
+ f.pinLocal("frontier");
267
+ assert.equal(sessionProfileLabel(resolveSessionProfile(options(f, "s-local"))), "frontier (local)");
268
+ f.pinLocal(undefined);
269
+ f.declareRepo("team");
270
+ assert.equal(sessionProfileLabel(resolveSessionProfile(options(f, "s-repo"))), "team (repo)");
271
+ f.declareRepo(undefined);
272
+ f.setActive("local");
273
+ assert.equal(sessionProfileLabel(resolveSessionProfile(options(f, "s-global"))), "local");
274
+ });
275
+
276
+ test("drift notice: shown once per distinct change of the defaults", () => {
277
+ const f = fixture();
278
+ f.pinLocal("frontier");
279
+ const opts = options(f, "session-a");
280
+ freezeSessionProfileAtStartup(opts);
281
+ assert.equal(inheritedProfileDriftNotice(opts), undefined, "no drift while the defaults match");
282
+ f.pinLocal("local");
283
+ assert.equal(
284
+ inheritedProfileDriftNotice(opts),
285
+ 'el Gentleman: the default profile changed to "local" (local), this session keeps "frontier" (local). Press Enter on a profile in /gentle:profiles to adopt it.',
286
+ );
287
+ assert.equal(inheritedProfileDriftNotice(opts), undefined, "the same change is not repeated");
288
+ f.pinLocal(undefined);
289
+ f.setActive("team");
290
+ assert.equal(
291
+ inheritedProfileDriftNotice(opts),
292
+ 'el Gentleman: the default profile changed to "team" (global), this session keeps "frontier" (local). Press Enter on a profile in /gentle:profiles to adopt it.',
293
+ );
294
+ assert.equal(inheritedProfileDriftNotice(opts), undefined);
295
+ assert.equal(readFrozenInheritedProfile("session-a")?.profile?.name, "frontier", "the notice never adopts the new default");
296
+ });
297
+
298
+ test("drift notice: an edit of the default profile's content counts as a change", () => {
299
+ const f = fixture();
300
+ f.setActive("frontier");
301
+ const opts = options(f, "session-a");
302
+ freezeSessionProfileAtStartup(opts);
303
+ f.editProfile("frontier", LOCAL);
304
+ assert.match(inheritedProfileDriftNotice(opts) ?? "", /changed to "frontier" \(global\), this session keeps "frontier" \(global\)/);
305
+ });
306
+
307
+ test("drift notice: memory is per session", () => {
308
+ const f = fixture();
309
+ f.pinLocal("frontier");
310
+ freezeSessionProfileAtStartup(options(f, "session-a"));
311
+ freezeSessionProfileAtStartup(options(f, "session-b"));
312
+ f.pinLocal("local");
313
+ assert.notEqual(inheritedProfileDriftNotice(options(f, "session-a")), undefined);
314
+ assert.notEqual(inheritedProfileDriftNotice(options(f, "session-b")), undefined);
315
+ });
316
+
317
+ test("drift notice covers a session frozen without a profile and a default that disappears", () => {
318
+ const f = fixture();
319
+ const none = options(f, "session-none");
320
+ freezeSessionProfileAtStartup(none);
321
+ f.pinLocal("frontier");
322
+ assert.equal(
323
+ inheritedProfileDriftNotice(none),
324
+ 'el Gentleman: the default profile changed to "frontier" (local), this session keeps no profile. Press Enter on a profile in /gentle:profiles to adopt it.',
325
+ );
326
+ const kept = options(f, "session-kept");
327
+ freezeSessionProfileAtStartup(kept);
328
+ f.pinLocal(undefined);
329
+ assert.equal(
330
+ inheritedProfileDriftNotice(kept),
331
+ 'el Gentleman: the default profile changed to no profile, this session keeps "frontier" (local). Press Enter on a profile in /gentle:profiles to adopt it.',
332
+ );
333
+ });
334
+
335
+ test("drift notice is never shown after an explicit Enter, in follow mode, or in a child", () => {
336
+ const f = fixture();
337
+ f.pinLocal("frontier");
338
+ const explicit = options(f, "session-explicit");
339
+ freezeSessionProfileAtStartup(explicit);
340
+ bindSessionProfile("session-explicit", "team", TEAM);
341
+ const follow = options(f, "session-follow", { [PROFILE_FOLLOW_ENV]: "1" });
342
+ const child = options(f, "session-child", { GENTLE_PI_AGENTS_CHILD: "1" });
343
+ f.pinLocal("local");
344
+ assert.equal(inheritedProfileDriftNotice(explicit), undefined);
345
+ assert.equal(inheritedProfileDriftNotice(follow), undefined);
346
+ assert.equal(inheritedProfileDriftNotice(child), undefined);
347
+ });
348
+
349
+ test("drift comparison on a session not frozen yet freezes the current default and reports nothing", () => {
350
+ const f = fixture();
351
+ f.pinLocal("frontier");
352
+ assert.equal(inheritedProfileDriftNotice(options(f, "session-a")), undefined);
353
+ assert.equal(readFrozenInheritedProfile("session-a")?.profile?.name, "frontier");
354
+ });
355
+
356
+ test("sessionProfileRoutingAt: explicit and frozen profiles apply to any target, a foreign repository included", () => {
357
+ const session = fixture();
358
+ session.pinLocal("frontier");
359
+ const foreign = fixture();
360
+ foreign.pinLocal("local");
361
+ const target = { cwd: foreign.cwd, configHome: foreign.configHome, resolveWorktree: foreign.resolveWorktree, foreignRepository: true };
362
+ assert.deepEqual(sessionProfileRoutingAt(resolveSessionProfile(options(session, "session-a")), target), FRONTIER);
363
+ bindSessionProfile("session-b", "team", TEAM);
364
+ assert.deepEqual(sessionProfileRoutingAt(resolveSessionProfile(options(session, "session-b")), target), TEAM);
365
+ });
366
+
367
+ test("sessionProfileRoutingAt: a session frozen without a profile keeps a foreign repository's own defaults", () => {
368
+ const session = fixture();
369
+ const foreign = fixture();
370
+ foreign.pinLocal("local");
371
+ const resolution = resolveSessionProfile(options(session, "session-a"));
372
+ session.pinLocal("frontier");
373
+ assert.deepEqual(sessionProfileRoutingAt(resolution, { cwd: foreign.cwd, configHome: foreign.configHome, resolveWorktree: foreign.resolveWorktree, foreignRepository: true }), LOCAL);
374
+ assert.equal(sessionProfileRoutingAt(resolution, { cwd: session.cwd, configHome: session.configHome, resolveWorktree: session.resolveWorktree }), undefined, "its own clone keeps no profile");
375
+ });
376
+
377
+ test("sessionProfileRoutingAt: a session frozen without a profile never picks up the global active profile in a foreign repository", () => {
378
+ const session = fixture();
379
+ const resolution = resolveSessionProfile(options(session, "session-a"));
380
+ const foreign = fixture();
381
+ foreign.setActive("team");
382
+ assert.equal(sessionProfileRoutingAt(resolution, { cwd: foreign.cwd, configHome: foreign.configHome, resolveWorktree: foreign.resolveWorktree, foreignRepository: true }), undefined);
383
+ foreign.declareRepo("local");
384
+ assert.deepEqual(sessionProfileRoutingAt(resolution, { cwd: foreign.cwd, configHome: foreign.configHome, resolveWorktree: foreign.resolveWorktree, foreignRepository: true }), LOCAL, "the foreign repository's own declaration still applies");
385
+ });
386
+
387
+ test("sessionProfileRoutingAt: follow reads the defaults of the target", () => {
388
+ const session = fixture();
389
+ session.pinLocal("frontier");
390
+ const other = fixture();
391
+ other.pinLocal("local");
392
+ const resolution = resolveSessionProfile(options(session, "session-a", { [PROFILE_FOLLOW_ENV]: "1" }));
393
+ assert.deepEqual(sessionProfileRoutingAt(resolution, { cwd: other.cwd, configHome: other.configHome, resolveWorktree: other.resolveWorktree }), LOCAL);
394
+ });