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,199 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { worktreeGitEnvironment } from "../lib/session-worktree-registry.ts";
8
+ import { canonicalWriterRoot, normalizeSurfaceEntry, overlappingSurfaces, surfacesOverlap, WriterSurfaceRegistry, writerSurfaceConflictMessage } from "../lib/writer-surfaces.ts";
9
+
10
+ // gentle-shell#1731 T4 (S2, AC6): concurrent writers are admitted only when
11
+ // their `## Allowed edit surfaces` are disjoint. Overlap is conservative: two
12
+ // entries overlap when some path could be covered by both, and an entry also
13
+ // covers everything under it, so uncertainty always reads as overlap.
14
+
15
+ test("normalizeSurfaceEntry strips list markers, backticks, ./ and trailing slashes", () => {
16
+ assert.equal(normalizeSurfaceEntry("lib/a.ts"), "lib/a.ts");
17
+ assert.equal(normalizeSurfaceEntry("- `docs/with space.md`"), "docs/with space.md");
18
+ assert.equal(normalizeSurfaceEntry("`./lib//a.ts`"), "lib/a.ts");
19
+ assert.equal(normalizeSurfaceEntry("lib\\nested\\a.ts"), "lib\\nested\\a.ts", "backslashes are kept for the fail-safe check");
20
+ assert.equal(normalizeSurfaceEntry("src/cafe\u0301.ts"), "src/caf\u00e9.ts", "entries compare in NFC");
21
+ assert.equal(normalizeSurfaceEntry("lib/./dir/"), "lib/dir");
22
+ assert.equal(normalizeSurfaceEntry(" 1. tests/x.test.ts "), "tests/x.test.ts");
23
+ });
24
+
25
+ test("normalizeSurfaceEntry refuses entries it cannot scope", () => {
26
+ for (const entry of ["", " ", ".", "./", "/abs/a.ts", "~/a.ts", "C:/a.ts", "lib/../a.ts", "``"]) {
27
+ assert.equal(normalizeSurfaceEntry(entry), undefined, entry);
28
+ }
29
+ });
30
+
31
+ test("disjoint files do not overlap", () => {
32
+ assert.equal(surfacesOverlap("lib/a.ts", "lib/b.ts"), false);
33
+ assert.equal(surfacesOverlap("lib/a.ts", "tests/a.ts"), false);
34
+ assert.equal(surfacesOverlap("lib/a.ts", "lib/a.ts.bak"), false);
35
+ assert.equal(surfacesOverlap("lib/a/b.ts", "lib/b/b.ts"), false);
36
+ });
37
+
38
+ test("the same file overlaps, whatever its spelling", () => {
39
+ assert.equal(surfacesOverlap("lib/a.ts", "lib/a.ts"), true);
40
+ assert.equal(surfacesOverlap("`lib/a.ts`", "./lib/a.ts"), true);
41
+ assert.equal(surfacesOverlap("- lib/a.ts", "lib//a.ts"), true);
42
+ assert.equal(surfacesOverlap("lib/A.ts", "lib/a.ts"), true, "case-insensitive filesystems make these one file");
43
+ });
44
+
45
+ test("a glob covering a file overlaps it", () => {
46
+ assert.equal(surfacesOverlap("lib/*.ts", "lib/a.ts"), true);
47
+ assert.equal(surfacesOverlap("lib/a.ts", "lib/*.ts"), true);
48
+ assert.equal(surfacesOverlap("tests/*.test.ts", "tests/writer-surfaces.test.ts"), true);
49
+ assert.equal(surfacesOverlap("lib/?.ts", "lib/a.ts"), true);
50
+ assert.equal(surfacesOverlap("lib/[ab].ts", "lib/b.ts"), true);
51
+ assert.equal(surfacesOverlap("lib/[!a].ts", "lib/b.ts"), true);
52
+ assert.equal(surfacesOverlap("lib/{a,b}.ts", "lib/b.ts"), true);
53
+ assert.equal(surfacesOverlap("lib/*.ts", "lib/a.md"), false);
54
+ assert.equal(surfacesOverlap("lib/{a,b}.ts", "lib/c.ts"), false);
55
+ assert.equal(surfacesOverlap("lib/*.ts", "tests/a.ts"), false);
56
+ });
57
+
58
+ test("a directory glob or bare directory covers anything under it", () => {
59
+ assert.equal(surfacesOverlap("lib/**", "lib/a.ts"), true);
60
+ assert.equal(surfacesOverlap("lib/**", "lib/deep/nested/a.ts"), true);
61
+ assert.equal(surfacesOverlap("lib/a.ts", "lib/**"), true);
62
+ assert.equal(surfacesOverlap("lib/**/*.ts", "lib/x/y.md"), true, "after ** the rest may sit under the other entry");
63
+ assert.equal(surfacesOverlap("lib", "lib/a.ts"), true);
64
+ assert.equal(surfacesOverlap("lib/", "lib/a.ts"), true);
65
+ assert.equal(surfacesOverlap("**/*.ts", "docs/a.md"), true);
66
+ assert.equal(surfacesOverlap("lib/**", "tests/a.ts"), false);
67
+ assert.equal(surfacesOverlap("lib/**", "libs/a.ts"), false);
68
+ });
69
+
70
+ test("two globs overlap unless their literal affixes prove them disjoint", () => {
71
+ assert.equal(surfacesOverlap("lib/*.ts", "lib/*.md"), false);
72
+ assert.equal(surfacesOverlap("lib/a*.ts", "lib/b*.ts"), false);
73
+ assert.equal(surfacesOverlap("lib/a*", "lib/*b"), true);
74
+ assert.equal(surfacesOverlap("lib/*.ts", "lib/x*"), true);
75
+ assert.equal(surfacesOverlap("lib/**", "lib/*.md"), true);
76
+ assert.equal(surfacesOverlap("lib/*/a.ts", "lib/x/*.ts"), true);
77
+ assert.equal(surfacesOverlap("lib/*/a.ts", "tests/*/a.ts"), false);
78
+ });
79
+
80
+ test("uncertain entries are treated as overlapping", () => {
81
+ assert.equal(surfacesOverlap("lib/../tests/a.ts", "docs/b.md"), true, "unscopable entry");
82
+ assert.equal(surfacesOverlap(".", "docs/b.md"), true, "repository root");
83
+ assert.equal(surfacesOverlap("lib/@(a|b).ts", "lib/c.ts"), true, "extglob syntax is not interpreted");
84
+ assert.equal(surfacesOverlap("lib/x[y", "lib/xzzy"), true, "an unclosed class falls back to affixes");
85
+ assert.equal(surfacesOverlap("lib/{a,{b,c}}.ts", "lib/d.ts"), true, "nested braces are not interpreted");
86
+ });
87
+
88
+ // Verify B1: a brace group spanning `/` must never be split into segments.
89
+ test("a brace group spanning a slash overlaps everything", () => {
90
+ assert.equal(surfacesOverlap("lib/{foo.ts,bar/baz.ts}", "lib/bar/baz.ts"), true);
91
+ assert.equal(surfacesOverlap("src/{a,b/c}.ts", "src/b/c.ts"), true);
92
+ assert.equal(surfacesOverlap("src/b/c.ts", "src/{a,b/c}.ts"), true);
93
+ assert.equal(surfacesOverlap("lib/{x,y/z}/w.ts", "docs/a.md"), true, "fail safe even across unrelated roots");
94
+ assert.equal(surfacesOverlap("lib/a}.ts", "lib/b.ts"), true, "an unbalanced brace is not interpreted");
95
+ const registry = new WriterSurfaceRegistry();
96
+ registry.claim("task-a", "/repo", ["lib/{foo.ts,bar/baz.ts}"]);
97
+ assert.deepEqual(registry.conflicts("/repo", ["lib/bar/baz.ts"]).map(conflict => conflict.taskId), ["task-a"]);
98
+ });
99
+
100
+ // Verify M1: lowercasing and class syntax (negation, POSIX classes) are never
101
+ // interpreted; any bracket in an entry overlaps everything.
102
+ test("any bracket class overlaps everything", () => {
103
+ assert.equal(surfacesOverlap("src/[^a].ts", "src/A.ts"), true);
104
+ assert.equal(surfacesOverlap("src/[!a-z].ts", "src/B.ts"), true);
105
+ assert.equal(surfacesOverlap("src/[[:alpha:]].ts", "src/b.ts"), true);
106
+ assert.equal(surfacesOverlap("lib/[ab].ts", "lib/c.ts"), true);
107
+ assert.equal(surfacesOverlap("lib/[a/b].ts", "docs/x.md"), true);
108
+ });
109
+
110
+ // Second verify (A1 fail-safe additions): a backslash may be an escape or a
111
+ // separator, so the entry overlaps everything.
112
+ test("any backslash overlaps everything", () => {
113
+ assert.equal(surfacesOverlap("src/foo\\*", "src/foo?"), true);
114
+ assert.equal(surfacesOverlap("lib\\a.ts", "docs/x.md"), true);
115
+ assert.equal(surfacesOverlap("docs/x.md", "lib\\a.ts"), true);
116
+ });
117
+
118
+ test("Unicode spellings compare in NFC and non-ASCII never meets a wildcard regex", () => {
119
+ assert.equal(surfacesOverlap("src/caf\u00e9.ts", "src/cafe\u0301.ts"), true, "NFC vs NFD is one file");
120
+ assert.equal(surfacesOverlap("src/?.ts", "src/\u{1F600}.ts"), true, "an astral character is one path character");
121
+ assert.equal(surfacesOverlap("src/\u{1F600}*.ts", "src/\u{1F600}x.ts"), true);
122
+ assert.equal(surfacesOverlap("src/caf\u00e9.ts", "src/cafe.ts"), false, "distinct literals stay disjoint");
123
+ });
124
+
125
+ // Second verify A1: the registry key is the canonical worktree root, so a
126
+ // subdirectory cwd, the root itself and a symlinked spelling share one key.
127
+ test("canonicalWriterRoot keys one worktree once and separate worktrees apart", (t) => {
128
+ const base = realpathSync(mkdtempSync(join(tmpdir(), "writer-root-")));
129
+ t.after(() => rmSync(base, { recursive: true, force: true }));
130
+ const repo = join(base, "repo");
131
+ mkdirSync(join(repo, "sub"), { recursive: true });
132
+ const git = (...args: string[]) => execFileSync("git", ["-C", repo, ...args], { env: worktreeGitEnvironment(), stdio: "ignore" });
133
+ git("init", "-q");
134
+ writeFileSync(join(repo, "sub", "a.ts"), "");
135
+ git("add", ".");
136
+ git("-c", "user.name=t", "-c", "user.email=t@example.invalid", "commit", "-q", "-m", "init");
137
+ const feature = join(base, "feature");
138
+ git("worktree", "add", "-q", feature);
139
+ const link = join(base, "link");
140
+ symlinkSync(repo, link);
141
+ assert.equal(canonicalWriterRoot(join(repo, "sub")), repo, "a subdirectory cwd keys to its worktree root");
142
+ assert.equal(canonicalWriterRoot(repo), repo);
143
+ assert.equal(canonicalWriterRoot(join(link, "sub")), repo, "a symlinked spelling keys to the real root");
144
+ assert.equal(canonicalWriterRoot(feature), feature, "a linked worktree keys to itself");
145
+ const registry = new WriterSurfaceRegistry();
146
+ registry.claim("task-a", canonicalWriterRoot(join(repo, "sub")), ["lib/a.ts"]);
147
+ assert.deepEqual(registry.conflicts(canonicalWriterRoot(link), ["lib/a.ts"]).map(conflict => conflict.taskId), ["task-a"]);
148
+ assert.deepEqual(registry.conflicts(canonicalWriterRoot(feature), ["lib/a.ts"]), [], "different worktrees never conflict");
149
+ const plain = join(base, "plain");
150
+ mkdirSync(plain);
151
+ symlinkSync(plain, join(base, "plain-link"));
152
+ assert.equal(canonicalWriterRoot(join(base, "plain-link"), () => undefined), plain, "without Git the key is the realpath");
153
+ assert.equal(canonicalWriterRoot(join(base, "missing"), () => undefined), join(base, "missing"), "an unresolvable path keeps its resolved spelling");
154
+ assert.equal(canonicalWriterRoot("/repo/sub", () => ({ root: "/repo", commonDir: "/repo/.git" })), "/repo", "an injected resolver decides the root");
155
+ });
156
+
157
+ // Like minimatch, `**` is a globstar only as a whole segment; inside a segment
158
+ // (`src/**.ts`) it matches within that one segment.
159
+ test("`**` inside a segment stays single-segment", () => {
160
+ assert.equal(surfacesOverlap("src/**.ts", "src/a.ts"), true);
161
+ assert.equal(surfacesOverlap("src/**.ts", "src/a/b.ts"), false);
162
+ assert.equal(surfacesOverlap("src/**", "src/a/b.ts"), true);
163
+ });
164
+
165
+ test("overlappingSurfaces lists every overlapping pair; an empty list claims everything", () => {
166
+ assert.deepEqual(overlappingSurfaces(["lib/a.ts", "docs/x.md"], ["lib/**", "tests/a.ts"]), [["lib/a.ts", "lib/**"]]);
167
+ assert.deepEqual(overlappingSurfaces(["lib/a.ts"], ["lib/b.ts", "tests/b.ts"]), []);
168
+ assert.deepEqual(overlappingSurfaces([], ["lib/b.ts"]), [[".", "lib/b.ts"]]);
169
+ });
170
+
171
+ test("the registry reports live overlapping writers in the same root only and releases them", () => {
172
+ const registry = new WriterSurfaceRegistry();
173
+ registry.claim("task-a", "/repo", ["lib/a.ts", "tests/a.test.ts"]);
174
+ registry.claim("task-b", "/repo", ["lib/b.ts"]);
175
+ registry.claim("task-c", "/repo-worktrees/feature", ["lib/a.ts"]);
176
+ assert.deepEqual(registry.conflicts("/repo", ["docs/x.md"]), []);
177
+ assert.deepEqual(registry.conflicts("/repo", ["lib/*.ts"]), [
178
+ { taskId: "task-a", root: "/repo", pairs: [["lib/*.ts", "lib/a.ts"]] },
179
+ { taskId: "task-b", root: "/repo", pairs: [["lib/*.ts", "lib/b.ts"]] },
180
+ ]);
181
+ assert.deepEqual(registry.conflicts("/elsewhere", ["lib/a.ts"]), [], "different worktree roots never conflict");
182
+ assert.deepEqual(registry.conflicts("/repo/", ["lib/b.ts"]).map(conflict => conflict.taskId), ["task-b"], "roots compare resolved");
183
+ assert.equal(registry.release("task-b"), true);
184
+ assert.equal(registry.release("task-b"), false, "release is idempotent");
185
+ assert.deepEqual(registry.conflicts("/repo", ["lib/b.ts"]), []);
186
+ assert.equal(registry.size, 2);
187
+ });
188
+
189
+ test("the conflict message names the live task, the overlapping entries and the ways forward", () => {
190
+ const message = writerSurfaceConflictMessage([{ taskId: "task-a", root: "/repo", pairs: [["lib/*.ts", "lib/a.ts"]] }]);
191
+ assert.match(message, /task task-a/);
192
+ assert.match(message, /`lib\/\*\.ts` overlaps `lib\/a\.ts`/);
193
+ assert.match(message, /\/repo/);
194
+ assert.match(message, /wait for/i);
195
+ assert.match(message, /narrow `## Allowed edit surfaces`/);
196
+ assert.match(message, /isolated worktree/);
197
+ const many = writerSurfaceConflictMessage([{ taskId: "t", root: "/repo", pairs: Array.from({ length: 8 }, (_, index) => [`a${index}`, `a${index}`] as [string, string]) }]);
198
+ assert.match(many, /and 3 more/, "long overlap lists stay bounded");
199
+ });
@@ -68,7 +68,7 @@ function fixture(t: test.TestContext, order = "owner-first", env: NodeJS.Process
68
68
  },
69
69
  } as unknown as ExtensionContext;
70
70
  let controller: yolo.YoloSessionController | undefined;
71
- const owner = () => { controller = yolo.registerYoloSessionPolicy(pi, env); };
71
+ const owner = () => { controller = yolo.registerYoloSessionPolicy(pi, { ...env, GENTLE_PI_CONFIG_HOME: home }); };
72
72
  const shell = () => gentleShell(pi, { ...env, GENTLE_PI_CONFIG_HOME: home, GENTLE_PI_SHELL_CHANGES_WATCH_MS: "off" }, {
73
73
  fetch: async () => { assert.fail("network disabled"); }, activeProfile: () => undefined, devBinary: () => undefined,
74
74
  });
@@ -111,6 +111,21 @@ async function settle(check: () => boolean) {
111
111
  }
112
112
  function text(view: View) { return view.render(140).map(stripAnsi).join("\n"); }
113
113
 
114
+ test("Customize selects Auto for this session and saves either elevated default without changing current mode", async t => {
115
+ const h=fixture(t,"owner-first",{},true);const open=await h.open();const view=h.view();selectYolo(view);
116
+ view.handleInput("j");view.handleInput("j");view.render(140);
117
+ assert.match(text(view),/Session permissions: AUTO/);
118
+ await h.apply("\r");assert.equal(await h.controller().mode(h.ctx),"auto");
119
+ view.handleInput("j");view.handleInput("j");view.handleInput("j");view.render(140);
120
+ assert.match(text(view),/Default permissions: AUTO/);
121
+ await h.apply("\r");assert.equal(h.controller().defaultMode(),"auto");assert.equal(await h.controller().mode(h.ctx),"auto");
122
+ view.handleInput("j");view.render(140);await h.apply("\r");
123
+ assert.equal(h.controller().defaultMode(),"yolo");assert.equal(await h.controller().mode(h.ctx),"auto");
124
+ assert.equal(JSON.parse(readFileSync(join(h.home,"permission-mode.json"),"utf8")).mode,"yolo");
125
+ for(const width of [1,20,60,140])assert.ok(view.render(width).every(line=>visibleWidth(line)<=width));
126
+ open.close();await open.result;
127
+ });
128
+
114
129
  for (const order of ["owner-first", "shell-first"]) test(`Editor YOLO menu and slash share one owner (${order})`, async t => {
115
130
  const h = fixture(t, order, {}, true);
116
131
  for (const name of ["vim-policy.json", "visual-settings.json", "visual-profiles.json", "runtime-guardrails.json", "history-capture-policy.json", "agent-profiles.json"]) writeFileSync(join(h.home, name), "{}\n");
@@ -187,11 +202,11 @@ test("off wins over pending menu on; child/headless/RPC cannot acquire adapter a
187
202
  assert.equal(await h.controller().active(h.ctx), false);
188
203
  open.close(); await open.result;
189
204
  assert.equal(typeof yolo.discoverYoloUiAdapter, "function");
190
- for (const variant of ["child", "headless", "rpc", "missing-clone"]) {
205
+ for (const variant of ["child", "headless", "rpc", "missing-workspace"]) {
191
206
  const f = fixture(t, "owner-first", variant === "child" ? { GENTLE_PI_AGENTS_CHILD: "1" } : {});
192
207
  if (variant === "headless") Object.assign(f.ctx, { hasUI: false });
193
208
  if (variant === "rpc") Object.assign(f.ctx, { mode: "rpc" });
194
- if (variant === "missing-clone") Object.assign(f.ctx, { cwd: "/" });
209
+ if (variant === "missing-workspace") Object.assign(f.ctx, { cwd: "/nonexistent-gentle-workspace" });
195
210
  const adapter = await yolo.discoverYoloUiAdapter(f.pi, f.ctx);
196
211
  if (adapter) { assert.equal(await adapter.read(), "UNAVAILABLE"); await adapter.toggle(); adapter.dispose(); }
197
212
  assert.equal(await f.controller().active(f.ctx), false, variant);
@@ -12,10 +12,17 @@ import { loadExtensionFromFactory } from "../node_modules/@earendil-works/pi-cod
12
12
  import { discoverYoloUiAdapter, registerYoloSessionPolicy, YOLO_STATUS_KEY, YOLO_STATUS_TEXT } from "../lib/yolo-session-policy.ts";
13
13
  import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
14
14
 
15
- test("complete Gentle AI extension registers and executes YOLO through the actual SDK loader", async () => {
15
+ // An empty env would resolve the developer's real ~/.pi/gentle-ai and inherit its persisted mode default.
16
+ function isolatedEnv(t: test.TestContext): NodeJS.ProcessEnv {
17
+ const home = mkdtempSync(join(tmpdir(), "yolo-runtime-config-"));
18
+ t.after(() => rmSync(home, { recursive: true, force: true }));
19
+ return { GENTLE_PI_CONFIG_HOME: home };
20
+ }
21
+
22
+ test("complete Gentle AI extension registers and executes YOLO through the actual SDK loader", async t => {
16
23
  const cwd = process.cwd();
17
24
  const runtime = createExtensionRuntime();
18
- const extension = await loadExtensionFromFactory(createGentleAiExtension({ nativeReviewCli: null, candidateViews: null, processEnv: {} }), cwd, createEventBus(), runtime);
25
+ const extension = await loadExtensionFromFactory(createGentleAiExtension({ nativeReviewCli: null, candidateViews: null, processEnv: isolatedEnv(t) }), cwd, createEventBus(), runtime);
19
26
  assert.equal(extension.handlers.get("session_start")?.length, 1, "original startup hook remains the sole handler");
20
27
  assert.equal(extension.handlers.get("session_shutdown")?.length, 1, "original shutdown hook remains the sole handler");
21
28
  const runner = new ExtensionRunner([extension], runtime, cwd, SessionManager.inMemory(cwd), {} as never);
@@ -40,8 +47,9 @@ test("complete Gentle AI extension registers and executes YOLO through the actua
40
47
  // Actual SDK factory loader, context/command runner and in-memory SessionManager.
41
48
  // Lifecycle events are dispatched through the real runner; the terminal UI is stubbed.
42
49
  // No providers, persisted sessions, global assets or real tool execution are needed.
43
- test("SDK-loaded YOLO command resets across actual runner lifecycle dispatch and extension replacement", async () => {
50
+ test("SDK-loaded YOLO command resets across actual runner lifecycle dispatch and extension replacement", async t => {
44
51
  const cwd = process.cwd();
52
+ const env = isolatedEnv(t);
45
53
  const manager = SessionManager.inMemory(cwd);
46
54
  const statuses = new Map<string, string | undefined>();
47
55
  const widgets = new Map<string, unknown>();
@@ -53,7 +61,7 @@ test("SDK-loaded YOLO command resets across actual runner lifecycle dispatch and
53
61
  const load = async () => {
54
62
  const runtime = createExtensionRuntime();
55
63
  const extension = await loadExtensionFromFactory((pi) => {
56
- const yolo = registerYoloSessionPolicy(pi, {});
64
+ const yolo = registerYoloSessionPolicy(pi, env);
57
65
  // Match production ownership: the host's existing hooks reset the controller.
58
66
  pi.on("session_start", (_event, ctx) => yolo.reset(ctx));
59
67
  pi.on("session_shutdown", (_event, ctx) => yolo.reset(ctx));
@@ -111,12 +119,32 @@ for (const shellFirst of [false, true]) test(`SDK loads both extensions on one b
111
119
  const statuses = new Map<string, string | undefined>();
112
120
  const notices: string[] = [];
113
121
  let renders = 0;
122
+ const renderWaiters = new Set<() => void>();
123
+ const text = () => view!.render(140).map(stripAnsi).join("\n");
124
+ // Footer publication precedes the asynchronous authority snapshot refresh.
125
+ // Observe the requested menu render, not elapsed time after setStatus.
126
+ const rendered = (expected: RegExp) => new Promise<void>((resolve, reject) => {
127
+ const check = () => {
128
+ if (!view || !expected.test(text())) return;
129
+ clearTimeout(deadline);
130
+ renderWaiters.delete(check);
131
+ resolve();
132
+ };
133
+ const deadline = setTimeout(() => {
134
+ renderWaiters.delete(check);
135
+ reject(new Error(`Menu did not request a render matching ${expected}:\n${text()}`));
136
+ }, 1_000);
137
+ renderWaiters.add(check);
138
+ });
114
139
  runner.setUIContext({
115
140
  theme, getAllThemes: () => [{ name: "dark" }], getTheme: () => theme,
116
141
  notify: (text: string) => notices.push(text), setStatus: (key: string, text?: string) => statuses.set(key, text), setWidget() {},
117
142
  custom(factory: (tui: unknown, theme: unknown, keys: unknown, done: () => void) => Component) {
118
143
  return new Promise<void>(resolve => {
119
- view = factory({ terminal: { rows: 30 }, requestRender: () => { renders++; } }, theme, {}, resolve);
144
+ view = factory({ terminal: { rows: 30 }, requestRender: () => {
145
+ renders++;
146
+ for (const check of [...renderWaiters]) check();
147
+ } }, theme, {}, resolve);
120
148
  opened();
121
149
  });
122
150
  },
@@ -132,15 +160,14 @@ for (const shellFirst of [false, true]) test(`SDK loads both extensions on one b
132
160
  await ready; assert.ok(view);
133
161
  for (let i = 0; i < 3; i++) view.handleInput?.("j");
134
162
  view.handleInput?.("\x1b[C"); view.handleInput?.("j"); view.handleInput?.("j");
135
- const text = () => view!.render(140).map(stripAnsi).join("\n");
136
163
  assert.match(text(), /YOLO: OFF · session only/);
164
+ const onRendered = rendered(/YOLO: ON/);
137
165
  view.handleInput?.("\r");
138
- for (let i = 0; i < 100 && statuses.get(YOLO_STATUS_KEY) !== YOLO_STATUS_TEXT; i++) await new Promise(resolve => setTimeout(resolve, 10));
166
+ await onRendered;
139
167
  assert.equal(statuses.get(YOLO_STATUS_KEY), YOLO_STATUS_TEXT);
140
- await new Promise(resolve => setTimeout(resolve, 30));
141
168
  assert.match(text(), /YOLO: ON/);
142
- await command.handler("disable", ctx);
143
- await new Promise(resolve => setTimeout(resolve, 30));
169
+ await Promise.all([rendered(/YOLO: OFF/), command.handler("disable", ctx)]);
170
+ assert.equal(statuses.get(YOLO_STATUS_KEY), undefined);
144
171
  assert.match(text(), /YOLO: OFF/);
145
172
  for (const width of [1, 20, 45, 60, 140]) assert.ok(view.render(width).every(line => visibleWidth(line) <= width));
146
173
  const adapter = await discoverYoloUiAdapter({
@@ -13,7 +13,17 @@ const USAGE = "Use /gentle:yolo enable|disable|status. State unchanged.";
13
13
  const STALE_MENU = "YOLO unchanged — the session changed while the menu was open.";
14
14
  const menuTitle = (state: "ON" | "OFF") => `🚀 Gentle YOLO 🔥 — full speed, destructive actions still ask (current: ${state})`;
15
15
 
16
- export function harness(env: NodeJS.ProcessEnv = {}) {
16
+ // An empty env would resolve the developer's real ~/.pi/gentle-ai and inherit its persisted mode default.
17
+ const isolatedConfigHomes: string[] = [];
18
+ process.once("exit", () => { for (const home of isolatedConfigHomes) rmSync(home, { recursive: true, force: true }); });
19
+ function isolatedConfigHome(): string {
20
+ const home = mkdtempSync(join(tmpdir(), "yolo-config-"));
21
+ isolatedConfigHomes.push(home);
22
+ return home;
23
+ }
24
+
25
+ export function harness(overrides: NodeJS.ProcessEnv = {}) {
26
+ const env: NodeJS.ProcessEnv = { GENTLE_PI_CONFIG_HOME: isolatedConfigHome(), ...overrides };
17
27
  const commands = new Map<string, { handler: (args: string, ctx: ExtensionContext) => Promise<void> }>();
18
28
  const handlers = new Map<string, Array<(event: unknown, ctx: ExtensionContext) => unknown>>();
19
29
  const tools: string[] = [];
@@ -132,12 +142,12 @@ test("revocation wins over an activation still awaiting Git identity", async ()
132
142
  }
133
143
  });
134
144
 
135
- test("child, headless, RPC and unidentified repository cannot activate", async () => {
136
- for (const variant of ["child", "headless", "rpc", "no-repository"]) {
145
+ test("child, headless, RPC and nonexistent workspace cannot activate", async () => {
146
+ for (const variant of ["child", "headless", "rpc", "no-workspace"]) {
137
147
  const h = harness(variant === "child" ? { GENTLE_PI_AGENTS_CHILD: "1" } : {});
138
148
  if (variant === "headless") Object.assign(h.ctx, { hasUI: false });
139
149
  if (variant === "rpc") Object.assign(h.ctx, { mode: "rpc" });
140
- if (variant === "no-repository") Object.assign(h.ctx, { cwd: "/" });
150
+ if (variant === "no-workspace") Object.assign(h.ctx, { cwd: "/nonexistent-gentle-workspace" });
141
151
  await h.command("enable"); assert.notEqual(h.statuses.get("gentle:yolo"), ON_TEXT);
142
152
  h.setSelect(async () => "enable");
143
153
  await h.command(""); assert.notEqual(h.statuses.get("gentle:yolo"), ON_TEXT, `${variant} menu enable fails closed`);
@@ -182,7 +192,12 @@ test("active YOLO waives only ordinary push after full destructive/guard evaluat
182
192
  await h.command("enable");
183
193
  assert.equal(await call("git push origin main"), undefined);
184
194
  assert.equal(h.confirmations(), 1);
185
- for (const command of ["git push origin main && rm -rf ./data", `sh -c "psql -c 'DROP TABLE users'"`, "git push origin main && git rebase main", "git push --force origin main", "git push origin main && npm publish", "git -C /other push origin main", "sh -c 'git push origin main'"]) {
195
+ // gentle-shell#2172: reversible compound delivery is waived too.
196
+ for (const command of ["git commit -m 'fix: x' && git push -u origin feature/x", "git push origin main && gh pr create --fill", "git -C /other push origin main", "git rebase origin/main && git push"]) {
197
+ assert.equal(await call(command), undefined, command);
198
+ }
199
+ assert.equal(h.confirmations(), 1, "waived commands never reach the confirmation dialog");
200
+ for (const command of ["git push origin main && rm -rf ./data", `sh -c "psql -c 'DROP TABLE users'"`, "git push origin main && git rebase -i main", "git push --force origin main", "git push origin main && npm publish", "git push origin :main", "sh -c 'git push origin main'"]) {
186
201
  assert.ok(await call(command), command);
187
202
  }
188
203
  await h.command("disable"); assert.ok(await call("git push origin main"));
@@ -259,3 +274,21 @@ test("a session replaced while the menu is open cannot receive the enable choice
259
274
  await h.command("");
260
275
  assert.equal(h.notices.at(-1), "YOLO OFF");
261
276
  });
277
+
278
+ test("bash_background goes through the same command confirmation and YOLO waiver as bash", async () => {
279
+ const h = harness();
280
+ const call = (command: string) => h.emit("tool_call", { toolName: "bash_background", input: { command } });
281
+ assert.deepEqual(await call("git push origin main"), { block: true, reason: "Gentle AI safety policy blocked the command because it was not confirmed." });
282
+ await h.command("enable");
283
+ assert.equal(await call("git push origin main"), undefined);
284
+ assert.ok(await call("git push origin main && rm -rf ./data"), "YOLO never waives a destructive command");
285
+ });
286
+
287
+ test("monitor goes through the same command confirmation and YOLO waiver as bash", async () => {
288
+ const h = harness();
289
+ const call = (command: string) => h.emit("tool_call", { toolName: "monitor", input: { command, timeout_seconds: 60 } });
290
+ assert.deepEqual(await call("git push origin main"), { block: true, reason: "Gentle AI safety policy blocked the command because it was not confirmed." });
291
+ await h.command("enable");
292
+ assert.equal(await call("git push origin main"), undefined);
293
+ assert.ok(await call("git push origin main && rm -rf ./data"), "YOLO never waives a destructive command");
294
+ });
@@ -42,7 +42,7 @@ test("legacy hosts without an event bus retain command registration but no menu
42
42
  const commands: string[] = [];
43
43
  const pi = { registerCommand: (name: string) => commands.push(name) } as unknown as ExtensionAPI;
44
44
  assert.ok(registerYoloSessionPolicy(pi, {}));
45
- assert.deepEqual(commands, ["gentle:yolo"], "bare yolo command is not registered");
45
+ assert.deepEqual(commands, ["gentle:mode", "gentle:yolo"], "bare commands are not registered");
46
46
  assert.equal(await discoverYoloUiAdapter(pi, {} as ExtensionContext), undefined);
47
47
  });
48
48