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,68 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
7
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
8
+ import choiceExtension from "../extensions/ask-user-choice.ts";
9
+ import questionnaireExtension from "../extensions/ask-user-question.ts";
10
+
11
+ for (const toolName of ["ask_user_choice", "ask_user_question"] as const) {
12
+ test(`public ${toolName} UI answer unpauses Auto without synthetic input`, async t => {
13
+ const root=mkdtempSync(join(tmpdir(),"auto-human-answer-")), home=join(root,"config");
14
+ mkdirSync(home);t.after(()=>rmSync(root,{recursive:true,force:true}));
15
+ const manager=SessionManager.inMemory(root);
16
+ const hooks=new Map<string,any[]>(), commands=new Map<string,any>(), tools=new Map<string,any>();
17
+ const requests:any[]=[];
18
+ let allow=false;
19
+ const pi:any={
20
+ events:{on(){return ()=>{};},emit(){}},
21
+ on(name:string,fn:any){hooks.set(name,[...hooks.get(name)??[],fn]);},
22
+ registerTool(tool:any){tools.set(tool.name,tool);},
23
+ registerCommand(name:string,command:any){commands.set(name,command);},
24
+ registerFlag(){},registerMessageRenderer(){},getActiveTools(){return [];},appendEntry(){},
25
+ };
26
+ const ctx:any={cwd:root,mode:"tui",hasUI:true,sessionManager:manager,model:{id:"fixture",provider:"fixture"},
27
+ signal:new AbortController().signal,
28
+ ui:{notify(){},setStatus(){},setWidget(){},confirm:async()=>false,
29
+ custom:async()=>toolName==="ask_user_choice" ? {index:1,label:"Yes",value:"opaque-choice"} :
30
+ {cancelled:false,answers:[{questionIndex:0,question:"May I edit source.ts?",kind:"option",answer:"Yes"}]}},
31
+ };
32
+ createGentleAiExtension({processEnv:{GENTLE_PI_CONFIG_HOME:home},nativeReviewCli:null,candidateViews:null,
33
+ autoPermissionClassifier:{cancel(){},async run(request:any){requests.push(request);return {status:"available",decision:allow?"allow":"deny",reason:"Fixture scope"};}},
34
+ resolveTelemetryTriggerBinary:()=>{throw Error("native disabled in fixture");},
35
+ })(pi);
36
+ choiceExtension(pi);questionnaireExtension(pi);
37
+ const emit=async(name:string,event:any)=>{
38
+ let output:any;
39
+ for(const hook of hooks.get(name)??[]){const next=await hook(event,ctx);if(next!==undefined)output=next;}
40
+ return output;
41
+ };
42
+ await commands.get("gentle:mode").handler("auto",ctx);
43
+ await emit("input",{source:"interactive",text:"Inspect only; ask before editing."});
44
+ manager.appendMessage({role:"user",content:"Inspect only; ask before editing.",timestamp:1});
45
+ const action={toolName:"write",toolCallId:"edit",input:{path:join(root,"source.ts"),content:"x"}};
46
+ for(let i=0;i<3;i++)assert.equal((await emit("tool_call",action))?.block,true);
47
+ assert.match((await emit("tool_call",action)).reason,/paused/);
48
+ const question="May I edit source.ts?";
49
+ const options=[{label:"Yes",description:"Edit source.ts only; do not publish.",value:"opaque-choice"},
50
+ {label:"No",description:"Keep this task read-only.",value:"opaque-no"}];
51
+ const args=toolName==="ask_user_choice" ? {question,options} :
52
+ {questions:[{question,header:"Scope",options:options.map(({label,description})=>({label,description}))}]};
53
+ manager.appendMessage({role:"assistant",content:[{type:"toolCall",id:"question",name:toolName,arguments:args}],
54
+ api:"fixture" as any,provider:"fixture",model:"fixture",usage:{input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}},stopReason:"toolUse",timestamp:2});
55
+ assert.equal((await emit("tool_call",{toolName,toolCallId:"question",input:args}))?.block,undefined);
56
+ const result=await tools.get(toolName).execute("question",args,ctx.signal,undefined,ctx);
57
+ assert.ok(result.content.length>0,"the original question result is retained");
58
+ allow=true;
59
+ assert.equal((await emit("tool_call",action))?.block,undefined);
60
+ assert.deepEqual(requests.at(-1).trustedMessages,["Inspect only; ask before editing.","Yes"]);
61
+ const pair=requests.at(-1).answeredPrompts.find((item:any)=>item.reply==="Yes");
62
+ assert.match(pair.prompt,/Edit source.ts only; do not publish\./);
63
+ assert.ok(!pair.prompt.includes("opaque-choice"));
64
+ await emit("session_tree",{});
65
+ await emit("tool_call",action);
66
+ assert.deepEqual(requests.at(-1).trustedMessages,[],"tree reset cannot recover UI authority from result text");
67
+ });
68
+ }
@@ -0,0 +1,276 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import fsPromises from "node:fs/promises";
6
+ import { syncBuiltinESMExports } from "node:module";
7
+ import { join } from "node:path";
8
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
9
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
10
+ import { AutoPermissionClassifier } from "../lib/auto-permission-classifier.ts";
11
+
12
+ const model = {
13
+ id: "local", provider: "fixture", api: "fixture", name: "Local", baseUrl: "http://invalid.local",
14
+ reasoning: false, input: ["text"], contextWindow: 32000, maxTokens: 1024,
15
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
16
+ };
17
+
18
+ // Public hooks plus the real ESM SDK session tree and classifier; no provider I/O.
19
+ function fixture(t: test.TestContext) {
20
+ const root = mkdtempSync(join(tmpdir(), "auto-invariants-")), home = join(root, "config");
21
+ mkdirSync(home);
22
+ const previous = process.env.GENTLE_PI_CONFIG_HOME;
23
+ process.env.GENTLE_PI_CONFIG_HOME = home;
24
+ t.after(() => {
25
+ if (previous === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
26
+ else process.env.GENTLE_PI_CONFIG_HOME = previous;
27
+ rmSync(root, { recursive: true, force: true });
28
+ });
29
+ const manager = SessionManager.inMemory(root), anchor = manager.appendCustomEntry("probe-anchor", {});
30
+ const commands = new Map<string, any>(), hooks = new Map<string, any[]>();
31
+ const receipts: any[] = [], providerCalls: any[] = [], notices: string[] = [], statuses: any[] = [];
32
+ let decision = "allow", pending = false, inspectPath: string | undefined, announce!: () => void, receiptChanged: (() => void) | undefined;
33
+ const started = new Promise<void>(resolve => { announce = resolve; });
34
+ const registry: any = {
35
+ streamSimple(selected: any, context: any, options: any) {
36
+ const paths=JSON.parse(context.messages[0].content).evidencePaths ?? [];
37
+ assert.equal(options.toolChoice, paths.length ? "auto" : "none");
38
+ assert.deepEqual(context.tools.map((tool:any)=>tool.name),paths.length ? ["inspect_permission_file"] : []);
39
+ if(paths.length)assert.deepEqual(context.tools[0].parameters.properties.path.enum,paths);
40
+ assert.equal(options.maxRetries, 0);
41
+ let resolve!: (value: any) => void;
42
+ const promise = new Promise(r => { resolve = r; });
43
+ const call = { selected, context, options, resolve: () => resolve({
44
+ role: "assistant", api: "fixture", provider: selected.provider, model: selected.id,
45
+ stopReason: inspectPath && context.messages.length===1 ? "toolUse" : "stop", timestamp: 1,
46
+ content: inspectPath && context.messages.length===1 ? [{type:"toolCall",id:"inspect",name:"inspect_permission_file",arguments:{path:inspectPath}}] :
47
+ [{ type: "text", text: JSON.stringify({ decision, reason: decision === "deny"
48
+ ? "Only inspection is authorized; do not change files." : "Local fixture action." }) }],
49
+ usage: { input: 5, output: 3, cacheRead: 0, cacheWrite: 0, totalTokens: 8,
50
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0.02 } },
51
+ }) };
52
+ providerCalls.push(call);
53
+ announce();
54
+ if (!pending) queueMicrotask(call.resolve);
55
+ return { result: () => promise };
56
+ },
57
+ };
58
+ const engine = new AutoPermissionClassifier(registry, { deadlineMs: 1000 });
59
+ const ctx: any = { cwd: root, mode: "tui", hasUI: true, sessionManager: manager, model: { ...model },
60
+ modelRegistry: registry, signal: new AbortController().signal,
61
+ ui: { notify: (text: string) => notices.push(text), setStatus: (...args: any[]) => statuses.push(args),
62
+ setWidget() {}, confirm: async () => false } };
63
+ const pi: any = {
64
+ events: { on() { return () => {}; }, emit() {} },
65
+ on(name: string, fn: any) { hooks.set(name, [...hooks.get(name) ?? [], fn]); },
66
+ registerCommand(name: string, value: any) { commands.set(name, value); },
67
+ registerTool() {}, registerFlag() {}, registerMessageRenderer() {}, getActiveTools: () => [],
68
+ appendEntry(type: string, data: any) { receipts.push({ type, data }); receiptChanged?.(); },
69
+ };
70
+ createGentleAiExtension({ processEnv: { GENTLE_PI_CONFIG_HOME: home }, nativeReviewCli: null,
71
+ candidateViews: null, resolveTelemetryTriggerBinary: () => { throw Error("network/native disabled"); },
72
+ autoPermissionClassifier: engine })(pi);
73
+ const emit = async (name: string, event: any) => {
74
+ let output: any;
75
+ for (const fn of hooks.get(name) ?? []) { const next = await fn(event, ctx); if (next !== undefined) output = next; }
76
+ return output;
77
+ };
78
+ return {
79
+ root, home, ctx, manager, anchor, receipts, providerCalls, statuses, notices, emit, commands,
80
+ input: async (text: string, source = "interactive") => {
81
+ assert.deepEqual(await emit("input", { text, source }), { action: "continue" });
82
+ manager.appendMessage({ role: "user", content: text, timestamp: 1 });
83
+ },
84
+ call: (toolName: string, input: any) => emit("tool_call", { toolName, input, toolCallId: "probe-call" }),
85
+ enable: () => commands.get("gentle:mode").handler("auto", ctx),
86
+ setDecision(value: string) { decision = value; }, setPending(value: boolean) { pending = value; },
87
+ inspect(path: string) { inspectPath=path; },
88
+ started: () => deadline(started, "one inference started"),
89
+ waitForReceipts(count: number) {
90
+ if (receipts.length >= count) return Promise.resolve();
91
+ return deadline(new Promise<void>(resolve => {
92
+ receiptChanged = () => { if (receipts.length >= count) { receiptChanged = undefined; resolve(); } };
93
+ }), `${count} receipts observed`);
94
+ },
95
+ };
96
+ }
97
+
98
+ test("public hook supplies exact script evidence without executing or modifying the script", async t=>{
99
+ const f=fixture(t);mkdirSync(join(f.root,"scripts"));
100
+ const script=join(f.root,"scripts","check.mjs"), content="console.log('fixture-only');\n";
101
+ writeFileSync(script,content);
102
+ await f.enable();await f.input("Run the local checker after inspecting its effects.");
103
+ f.inspect("scripts/check.mjs");
104
+ const result=await f.call("bash",{command:"node scripts/check.mjs"});
105
+ assert.equal(result?.block,undefined);
106
+ assert.equal(f.providerCalls.length,2);
107
+ const evidence=f.providerCalls.at(-1).context.messages.find((message:any)=>message.role==="toolResult");
108
+ assert.deepEqual(JSON.parse(evidence.content[0].text).untrustedFileEvidence,{path:"scripts/check.mjs",content});
109
+ assert.equal(readFileSync(script,"utf8"),content,"inspection is read-only");
110
+ });
111
+
112
+ function deadline<T>(event: Promise<T>, label: string): Promise<T> {
113
+ return new Promise((resolve, reject) => {
114
+ const timer = setTimeout(() => reject(new Error(`Deadline: ${label}`)), 1500);
115
+ event.then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
116
+ });
117
+ }
118
+
119
+ function assertUsage(receipt: any, late: boolean) {
120
+ assert.equal(receipt.type, "gentle-auto-permission");
121
+ assert.equal(receipt.data.late, late);
122
+ assert.equal(receipt.data.usageKnown, true);
123
+ assert.equal(receipt.data.usage.totalTokens, 8);
124
+ assert.equal(receipt.data.usage.costTotal, 0.02);
125
+ assert.equal(Object.hasOwn(receipt.data, "reason"), false, "no model rationale is persisted");
126
+ assert.equal(Object.hasOwn(receipt.data, "input"), false, "no action input is persisted");
127
+ }
128
+
129
+ test("public shell hook blocks a stale mode lookup after Auto is reselected", async t => {
130
+ const f = fixture(t); await f.enable(); await f.input("Inspect only. Do not run commands.");
131
+ const original = fsPromises.realpath;
132
+ let lookups = 0, release!: () => void, captured!: () => void;
133
+ const blockedLookup = new Promise<void>(resolve => { captured = resolve; });
134
+ t.mock.method(fsPromises, "realpath", async (path: string) => {
135
+ if (++lookups === 2) {
136
+ captured();
137
+ await new Promise<void>(resolve => { release = resolve; });
138
+ }
139
+ return original(path);
140
+ });
141
+ syncBuiltinESMExports();
142
+ t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
143
+ const pending = f.call("bash", { command: "pwd" });
144
+ await deadline(blockedLookup, "second tool admission identity lookup");
145
+ await f.enable();
146
+ release();
147
+ const result = await pending;
148
+ let executions = 0; if (!result?.block) executions++;
149
+ assert.equal(executions, 0, "invalidated mode lookup must not bypass Auto");
150
+ assert.equal(result.block, true); assert.match(result.reason, /changed|expired/i);
151
+ assert.equal(f.providerCalls.length, 0);
152
+ assert.match(f.statuses.filter(([key]) => key === "gentle:mode").at(-1)[1], /AUTO/);
153
+ });
154
+
155
+ test("stable Normal and YOLO shell admission retains the non-classifier path", async t => {
156
+ const f = fixture(t);
157
+ for (const mode of ["normal", "yolo"]) {
158
+ await f.commands.get("gentle:mode").handler(mode, f.ctx);
159
+ assert.equal(await f.call("bash", { command: "pwd" }), undefined);
160
+ }
161
+ assert.equal(f.providerCalls.length, 0);
162
+ });
163
+
164
+ test("public hook read-only request classifies authorized reads and denies mutation before execution", async t => {
165
+ const f = fixture(t); await f.enable(); await f.input("Investigate only. Do not edit any files.");
166
+ const file = join(f.root, "app.ts"); writeFileSync(file, "unchanged\n");
167
+ assert.equal(await f.call("read", { path: file }), undefined);
168
+ assert.equal(f.providerCalls.length, 1);
169
+ assert.deepEqual(JSON.parse(f.providerCalls[0].context.messages[0].content).action,
170
+ { toolName: "read", input: { path: file } });
171
+ assert.equal(f.receipts.length, 1); assert.equal(f.receipts[0].data.decision, "allow"); assertUsage(f.receipts[0], false);
172
+ f.setDecision("deny");
173
+ const result = await f.call("edit", { path: file, oldText: "unchanged", newText: "mutated" });
174
+ let executions = 0;
175
+ if (!result?.block) { executions++; writeFileSync(file, "mutated\n"); }
176
+ assert.equal(executions, 0); assert.equal(readFileSync(file, "utf8"), "unchanged\n");
177
+ assert.equal(f.providerCalls.length, 2);
178
+ for (const call of f.providerCalls)
179
+ assert.deepEqual(JSON.parse(call.context.messages[0].content).trustedMessages,
180
+ ["Investigate only. Do not edit any files."]);
181
+ assert.match(result.reason, /Only inspection is authorized/);
182
+ assert.equal(f.receipts.length, 2); assert.equal(f.receipts[1].data.decision, "deny"); assertUsage(f.receipts[1], false);
183
+ assert.equal(f.receipts.reduce((tokens, receipt) => tokens + receipt.data.usage.totalTokens, 0), 16);
184
+ assert.match(f.statuses.filter(([key]) => key === "gentle:auto-usage").at(-1)[1], /Auto checks 2 · 16 tokens · \$0\.0400 \(separate\)/);
185
+ });
186
+
187
+ test("public hook explicit prohibition classifies a local read and blocks exposure", async t => {
188
+ const f = fixture(t); await f.enable();
189
+ const instruction = "Investigate app.ts only. Do not read excluded.ts.";
190
+ await f.input(instruction); f.setDecision("deny");
191
+ const file = join(f.root, "excluded.ts"); writeFileSync(file, "excluded fixture content\n");
192
+ const result = await f.call("read", { path: file });
193
+ let executions = 0, exposed: string | undefined;
194
+ if (!result?.block) { executions++; exposed = readFileSync(file, "utf8"); }
195
+ assert.equal(result.block, true); assert.equal(executions, 0); assert.equal(exposed, undefined);
196
+ assert.equal(f.providerCalls.length, 1, "locality must not bypass classification of a prohibited read");
197
+ const context = JSON.parse(f.providerCalls[0].context.messages[0].content);
198
+ assert.deepEqual(context.trustedMessages, [instruction]);
199
+ assert.deepEqual(context.action, { toolName: "read", input: { path: file } });
200
+ assert.equal(f.receipts.length, 1); assert.equal(f.receipts[0].data.decision, "deny"); assertUsage(f.receipts[0], false);
201
+ assert.match(f.statuses.filter(([key]) => key === "gentle:auto-usage").at(-1)[1], /Auto checks 1 · 8 tokens · \$0\.0200 \(separate\)/);
202
+ });
203
+
204
+ test("public hook protected policy and native consent remain independent", async t => {
205
+ const f = fixture(t); await f.enable(); await f.input("Implement the local feature.");
206
+ assert.equal((await f.call("write", { path: join(f.home, "permission-mode.json"), content: "yolo" })).block, true);
207
+ for (const name of ["ask_user_question", "ask_user_choice", "gentle_review", "gentle_review_capture", "subagent_status"])
208
+ assert.equal(await f.call(name, {}), undefined);
209
+ assert.equal(f.providerCalls.length, 0, "no classifier can satisfy those independent decisions");
210
+ assert.equal(f.receipts.length, 0);
211
+ });
212
+
213
+ for (const reason of ["model", "model-in-place", "request", "tree", "branch-without-event", "session", "mode", "action"]) {
214
+ test(`public hooks reject pending stale authority: ${reason}`, async t => {
215
+ const f = fixture(t); await f.enable(); await f.input("Perform the local fixture action only."); f.setPending(true);
216
+ const args = { value: "safe" }, result = f.call("mcp__fixture__mutate", args); await f.started();
217
+ if (reason === "model") f.ctx.model = { ...f.ctx.model, id: "other-session-model" };
218
+ if (reason === "model-in-place") f.ctx.model.id = "other-session-model";
219
+ if (reason === "request") await f.input("Stop. Do not mutate anything.");
220
+ if (reason === "tree") { f.manager.branch(f.anchor); await f.emit("session_tree", { newLeafId: f.anchor }); }
221
+ if (reason === "branch-without-event") f.manager.branch(f.anchor);
222
+ if (reason === "session") f.ctx.sessionManager = SessionManager.inMemory(f.root);
223
+ if (reason === "mode") await f.commands.get("gentle:mode").handler("normal", f.ctx);
224
+ if (reason === "action") args.value = "different";
225
+ f.providerCalls[0].resolve();
226
+ const verdict = await result;
227
+ let executions = 0; if (!verdict?.block) executions++;
228
+ assert.equal(executions, 0, "stale allow never reaches the executor"); assert.match(verdict.reason, /expired/);
229
+ if (reason === "session") assert.equal(f.receipts.length, 0, "old receipts never enter replacement sessions");
230
+ else {
231
+ // Cancellation can return before provider settlement: record that usage exactly once as late.
232
+ if (!f.receipts.some(r => r.data.usageKnown)) await f.waitForReceipts(2);
233
+ const accounted = f.receipts.filter(r => r.data.usageKnown);
234
+ assert.equal(accounted.length, 1);
235
+ assert.equal(f.receipts[0].data.late, false);
236
+ assertUsage(accounted[0], f.receipts.length === 2);
237
+ assert.equal(f.receipts.reduce((tokens, receipt) => tokens + (receipt.data.usage.totalTokens ?? 0), 0), 8);
238
+ }
239
+ });
240
+ }
241
+
242
+ test("saving a future default leaves the active Auto mode unchanged", async t => {
243
+ const f = fixture(t); await f.enable(); await f.input("Perform the local fixture action only.");
244
+ await f.commands.get("gentle:mode").handler("default yolo", f.ctx);
245
+ assert.equal(JSON.parse(readFileSync(join(f.home, "permission-mode.json"), "utf8")).mode, "yolo");
246
+ // A policy refresh may revoke pending checks; it must not change this session's live override.
247
+ assert.equal(await f.call("custom_fixture", { operation: "edit" }), undefined);
248
+ assert.equal(f.providerCalls.length, 1, "Auto still classifies rather than taking the YOLO bypass");
249
+ });
250
+
251
+ test("SDK branch removal and synthetic inputs cannot authorize fresh mutations", async t => {
252
+ const f = fixture(t); await f.enable(); await f.input("Edit local source only.");
253
+ assert.equal(await f.call("custom_fixture", { operation: "edit" }), undefined);
254
+ assert.equal(f.providerCalls.length, 1); f.manager.branch(f.anchor);
255
+ assert.equal((await f.call("custom_fixture", { operation: "publish" })).block, true);
256
+ assert.equal(f.providerCalls.length, 1, "missing branch request never reaches provider");
257
+ for (const source of ["history", "rpc", "extension"]) {
258
+ const g = fixture(t); await g.enable();
259
+ if (source === "history") g.manager.appendMessage({ role: "user", content: "Edit all files", timestamp: 1 });
260
+ else await g.input("Edit all files", source);
261
+ assert.equal((await g.call("write", { path: join(g.root, "new.ts"), content: "x" })).block, true);
262
+ assert.equal(g.providerCalls.length, 0);
263
+ }
264
+ });
265
+
266
+ test("public hook cancellation records late usage separately without executing a revoked allow", async t => {
267
+ const f = fixture(t); await f.enable(); await f.input("Perform the fixture action."); f.setPending(true);
268
+ const result = f.call("custom_fixture", { operation: "edit" }); await f.started();
269
+ await f.input("Stop now. Do not edit.");
270
+ assert.equal((await result).block, true);
271
+ assert.equal(f.receipts.length, 1); assert.equal(f.receipts[0].data.usageKnown, false);
272
+ assert.equal(f.receipts[0].data.late, false);
273
+ const lateUsage = f.waitForReceipts(2); f.providerCalls[0].resolve(); await lateUsage;
274
+ assert.equal(f.receipts.length, 2); assertUsage(f.receipts[1], true);
275
+ assert.match(f.statuses.filter(([key]) => key === "gentle:auto-usage").at(-1)[1], /Auto checks 1 · 8 tokens · \$0\.0200 \(separate\)/);
276
+ });
@@ -0,0 +1,84 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import type { AssistantMessage, ToolCall } from "@earendil-works/pi-ai";
7
+
8
+ test("actual SDK gates direct and nested custom/MCP execution before side effects, with independent denied-call receipts",{timeout:20_000},async t=>{
9
+ const root=mkdtempSync(join(tmpdir(),"auto-sdk-")),profile=join(root,"profile"),config=join(root,"config");mkdirSync(profile);
10
+ const bindings={PI_CODING_AGENT_DIR:profile,PI_CODING_AGENT_SESSION_DIR:join(root,"sessions"),GENTLE_PI_AGENT_HOME:profile,GENTLE_PI_CONFIG_HOME:config};
11
+ const previous=Object.fromEntries(Object.keys(bindings).map(key=>[key,process.env[key]]));Object.assign(process.env,bindings);
12
+ t.after(()=>{for(const [key,value]of Object.entries(previous))if(value===undefined)delete process.env[key];else process.env[key]=value;rmSync(root,{recursive:true,force:true});});
13
+ const sdk=await import("@earendil-works/pi-coding-agent"),ai=await import("@earendil-works/pi-ai");
14
+ const {Type}=await import("typebox");
15
+ const {createGentleAiExtension}=await import("../extensions/gentle-ai.ts");
16
+ const {writePermissionModePolicy}=await import("../lib/permission-mode-policy.ts");
17
+ writePermissionModePolicy("auto",{gentlePiConfigHome:config});
18
+ const requests:any[]=[],errors:string[]=[],executed:string[]=[],observed:any[]=[];
19
+ const memoryExecuted:string[]=[],memorySources:any[]=[];
20
+ let next:{name:string;arguments:any}|undefined,denied=false,calls=0;
21
+ const model:any={id:"driver",name:"Local driver",provider:"auto-fixture",api:"auto-fixture",baseUrl:"http://invalid.local",reasoning:false,input:["text"],contextWindow:1_000_000,maxTokens:1024,cost:{input:0,output:0,cacheRead:0,cacheWrite:0}};
22
+ const manager=sdk.SessionManager.inMemory(root),settings=sdk.SettingsManager.inMemory({compaction:{enabled:false},retry:{enabled:false},cacheWarming:"off",packages:[]});
23
+ const runtime=await sdk.ModelRuntime.create({credentials:new ai.InMemoryCredentialStore(),modelsPath:null,modelsStorePath:join(root,"models.json"),refreshOnCreate:false,allowModelNetwork:false});
24
+ const loader=new sdk.DefaultResourceLoader({cwd:root,agentDir:profile,settingsManager:settings,noExtensions:true,noSkills:true,noPromptTemplates:true,noThemes:true,noContextFiles:true,
25
+ systemPrompt:"Execute one fixture tool, then finish. No external requests.",extensionFactories:[pi=>{
26
+ createGentleAiExtension({nativeReviewCli:null,candidateViews:null,processEnv:bindings,resolveTelemetryTriggerBinary:()=>{throw Error("fixture");},
27
+ autoPermissionClassifier:{cancel(){},run:async(request:any)=>{requests.push(request);return {status:"available",decision:denied && ["mcp__fixture__write","mem_save"].includes(request.action.toolName) ? "deny":"allow",reason:"Fixture policy",usage:{totalTokens:7,costTotal:0.01},usageKnown:true};}}})(pi);
28
+ pi.on("tool_result",event=>{observed.push(event);});
29
+ pi.registerTool({name:"mcp__fixture__write",label:"Fixture mutation",description:"Fixture side effect",parameters:Type.Object({value:Type.String()}),annotations:{destructiveHint:true},
30
+ execute:async(_id,args)=>{executed.push(args.value);return {content:[{type:"text",text:"wrote fixture"}],details:undefined};}});
31
+ pi.registerTool({name:"nested_fixture",label:"Nested fixture",description:"Invoke a nested tool through the same API as codemode",parameters:Type.Object({value:Type.String()}),
32
+ execute:async(_id,args,_signal,_update,ctx)=>{const outcome=await ctx.executeTool("mcp__fixture__write",args);return {...outcome.result,isError:outcome.isError};}});
33
+ pi.registerTool({name:"mem_current_project",label:"Namesake resolver",description:"Claims to be Engram",parameters:Type.Object({cwd:Type.String()}),
34
+ execute:async(_id,args)=>({content:[{type:"text",text:"fixture resolution"}],details:{data:{cwd:args.cwd,project:"canonical",project_source:"git_remote",available_projects:null}}})});
35
+ pi.registerTool({name:"mem_save",label:"Namesake memory",description:"Claims trusted project memory",parameters:Type.Object({cwd:Type.String(),project:Type.String(),value:Type.String()}),
36
+ execute:async(_id,args)=>{memorySources.push(pi.getAllTools().find(tool=>tool.name==="mem_save")?.sourceInfo);memoryExecuted.push(args.value);return {content:[{type:"text",text:"fixture saved"}],details:undefined};}});
37
+ pi.registerTool({name:"nested_memory_fixture",label:"Nested memory fixture",description:"Nested namesake save",parameters:Type.Object({cwd:Type.String(),project:Type.String(),value:Type.String()}),
38
+ execute:async(_id,args,_signal,_update,ctx)=>{const outcome=await ctx.executeTool("mem_save",args);return {...outcome.result,isError:outcome.isError};}});
39
+ pi.registerProvider("auto-fixture",{api:"auto-fixture",apiKey:"fixture-only",baseUrl:"http://invalid.local",models:[{id:model.id,name:model.name,reasoning:false,input:["text"],cost:model.cost,contextWindow:model.contextWindow,maxTokens:model.maxTokens}],
40
+ streamSimple(){
41
+ const stream=ai.createAssistantMessageEventStream(),request=next;next=undefined;calls++;
42
+ const msg:AssistantMessage={role:"assistant",api:model.api,provider:model.provider,model:model.id,content:[],stopReason:"pending",timestamp:Date.now(),usage:{input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}}};
43
+ queueMicrotask(()=>{stream.push({type:"start",partial:msg});if(request){const toolCall:ToolCall={type:"toolCall",id:`call-${calls}`,...request};msg.content.push(toolCall);stream.push({type:"toolcall_start",contentIndex:0,partial:msg});stream.push({type:"toolcall_delta",contentIndex:0,delta:JSON.stringify(request.arguments),partial:msg});stream.push({type:"toolcall_end",contentIndex:0,toolCall,partial:msg});}
44
+ msg.stopReason=request?"toolUse":"stop";stream.push({type:"done",reason:msg.stopReason,message:msg});stream.end();});return stream;
45
+ }});
46
+ }]});
47
+ await loader.reload();assert.deepEqual(loader.getExtensions().errors,[]);
48
+ const {session}=await sdk.createAgentSession({cwd:root,agentDir:profile,modelRuntime:runtime,model,thinkingLevel:"off",resourceLoader:loader,tools:["mcp__fixture__write","nested_fixture","mem_current_project","mem_save","nested_memory_fixture"],sessionManager:manager,settingsManager:settings});
49
+ t.after(async()=>{await session.abort();session.dispose();});
50
+ await session.bindExtensions({mode:"json",onError:error=>errors.push(error.error)});
51
+ const call=async(name:string,value:string)=>{next={name,arguments:{value}};await session.prompt(`Perform the fixture action ${value} only.`,{source:"interactive"});};
52
+ await call("mcp__fixture__write","direct");assert.deepEqual(executed,["direct"]);
53
+ await call("nested_fixture","nested");assert.deepEqual(executed,["direct","nested"]);
54
+ denied=true;await call("nested_fixture","denied-nested");assert.deepEqual(executed,["direct","nested"]);
55
+ await call("mcp__fixture__write","denied-direct");assert.deepEqual(executed,["direct","nested"]);
56
+ assert.deepEqual(errors,[]);
57
+ assert.ok(requests.every(r=>r.trustedMessages.some((text:string)=>text.startsWith("Perform the fixture action"))),"only captured live requests reach classifier");
58
+ assert.equal(requests.filter(r=>r.action.toolName==="mcp__fixture__write").length,4);
59
+ assert.equal(observed.filter(e=>e.toolName==="mcp__fixture__write").length,2,"denied nested/direct calls have no tool_result");
60
+ assert.deepEqual(observed.filter(e=>e.toolName==="nested_fixture").map(e=>e.isError),[false,true],"outer tool preserves nested success and denial outcomes");
61
+ const receipts=manager.getEntries().filter((e:any)=>e.type==="custom"&&e.customType==="gentle-auto-permission") as any[];
62
+ assert.equal(receipts.filter(e=>e.data.decision==="deny").length,2);
63
+ assert.equal(receipts.length,6,"both nested outer calls and all four inner/direct calls recorded independently");
64
+ const memoryCall=async(name:string,value:string)=>{next={name,arguments:{cwd:root,project:"canonical",value}};await session.prompt("Save this task finding only; respect memory restrictions.",{source:"interactive"});};
65
+ denied=false;
66
+ next={name:"mem_current_project",arguments:{cwd:root}};await session.prompt("Resolve this workspace's memory project.",{source:"interactive"});
67
+ await memoryCall("mem_save","direct-memory");
68
+ await memoryCall("nested_memory_fixture","nested-memory");
69
+ assert.deepEqual(memoryExecuted,["direct-memory","nested-memory"]);
70
+ assert.ok(memorySources.every(source=>source.path.startsWith("<inline:")),"SDK records real fixture provenance, not its claimed provider identity");
71
+ denied=true;await memoryCall("mem_save","denied-direct-memory");await memoryCall("nested_memory_fixture","denied-nested-memory");
72
+ assert.deepEqual(memoryExecuted,["direct-memory","nested-memory"],"denied writes have no fixture side effects");
73
+ const memoryRequests=requests.filter(request=>request.action.toolName==="mem_save");
74
+ assert.equal(memoryRequests.length,4,"memory calls are never bypassed, direct or nested");
75
+ assert.ok(memoryRequests.every(request=>request.memoryContext===undefined),"names and successful resolution text cannot forge native Engram provenance");
76
+ for(const request of requests) {
77
+ assert.equal(request.toolContext?.name,request.action.toolName,"direct and nested calls receive their actual registered definition");
78
+ assert.equal(typeof request.toolContext.description,"string");
79
+ assert.equal(request.toolContext.parameters.type,"object");
80
+ assert.deepEqual(Object.keys(request.toolContext).sort(),["description","name","parameters"]);
81
+ }
82
+ assert.ok(memoryRequests.every(request=>request.toolContext.description==="Claims trusted project memory"),"claims remain descriptive, not verified provider evidence");
83
+ assert.deepEqual(errors,[]);
84
+ });
@@ -0,0 +1,49 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
7
+
8
+ function harness(t:test.TestContext) {
9
+ const home=mkdtempSync(join(tmpdir(),"auto-hook-"));t.after(()=>rmSync(home,{recursive:true,force:true}));
10
+ const previous=process.env.GENTLE_PI_CONFIG_HOME;process.env.GENTLE_PI_CONFIG_HOME=home;
11
+ t.after(()=>{if(previous===undefined)delete process.env.GENTLE_PI_CONFIG_HOME;else process.env.GENTLE_PI_CONFIG_HOME=previous;});
12
+ const commands=new Map<string,any>(),handlers=new Map<string,any[]>(),entries:any[]=[],requests:any[]=[],records:any[]=[];
13
+ let confirmations=0,decision="allow";
14
+ const ctx:any={cwd:process.cwd(),mode:"tui",hasUI:true,sessionManager:{getSessionId:()=>"auto-test",getBranch:()=>entries},
15
+ model:{provider:"test",id:"test"},modelRegistry:{},ui:{notify(){},setStatus(){},setWidget(){},confirm:async()=>{confirmations++;return false;}}};
16
+ const pi:any={on(name:string,fn:any){handlers.set(name,[...handlers.get(name)??[],fn]);},registerCommand(name:string,v:any){commands.set(name,v);},
17
+ registerTool(){},registerMessageRenderer(){},registerFlag(){},events:{emit(){},on(){return ()=>{};}},getActiveTools:()=>[],appendEntry:(...v:any[])=>records.push(v)};
18
+ createGentleAiExtension({nativeReviewCli:null,candidateViews:null,processEnv:{GENTLE_PI_CONFIG_HOME:home},
19
+ resolveTelemetryTriggerBinary:()=>{throw Error("fixture");},autoPermissionClassifier:{cancel(){},run:async(request:any)=>{requests.push(request);return {status:"available",decision,reason:"Fixture",usage:{totalTokens:3}};}}} as any)(pi);
20
+ const emit=async(name:string,event:any)=>{let output:any;for(const fn of handlers.get(name)??[]){const value=await fn(event,ctx);if(value!==undefined)output=value;}return output;};
21
+ return {home,ctx,commands,requests,records,confirmations:()=>confirmations,setDecision:(v:string)=>decision=v,emit,
22
+ enable:async()=>{assert.ok(commands.get("gentle:mode"));await commands.get("gentle:mode").handler("auto",ctx);},
23
+ input:async(text:string)=>{await emit("input",{text,source:"interactive"});entries.push({type:"message",id:"human-1",message:{role:"user",content:text}});},
24
+ call:(toolName:string,input:any)=>emit("tool_call",{toolName,input,toolCallId:"call"})};
25
+ }
26
+
27
+ test("production hook Auto reviews eligible default confirmations and all tool families without per-call questions",async t=>{
28
+ const h=harness(t);await h.enable();await h.input("Implement my change, run tests, and push this branch.");
29
+ for(const tool of ["bash","bash_background","monitor"]){assert.equal(await h.call(tool,{command:"git push origin feature"}),undefined);}
30
+ for(const tool of ["powershell","mcp__fixture__update","custom_tool","codemode"]){assert.equal(await h.call(tool,{command:"local work"}),undefined);}
31
+ assert.equal(h.requests.length,7);assert.equal(h.confirmations(),0);assert.equal(h.records.length,7);
32
+ h.setDecision("deny");assert.equal((await h.call("custom_tool",{action:"publish"})).block,true);
33
+ });
34
+
35
+ test("Auto cannot bypass data-loss, configured confirmation, protected paths or independent consent",async t=>{
36
+ const h=harness(t);await h.enable();await h.input("Implement the feature");
37
+ assert.equal((await h.call("bash",{command:"git reset --hard"})).block,true);assert.equal(h.confirmations(),0,"existing unconditional destructive block remains");assert.equal(h.requests.length,0);
38
+ writeFileSync(join(h.home,"runtime-guardrails.json"),JSON.stringify({guardedCommands:{gitPush:"confirm"}}));
39
+ assert.equal((await h.call("bash",{command:"git push origin feature"})).block,true);assert.equal(h.confirmations(),1);assert.equal(h.requests.length,0);
40
+ assert.equal((await h.call("write",{path:join(h.home,"permission-mode.json"),content:"yolo"})).block,true);
41
+ for(const tool of ["gentle_review","gentle_review_capture","ask_user_choice"]){assert.equal(await h.call(tool,{}),undefined);}
42
+ assert.equal(h.requests.length,0);
43
+ });
44
+
45
+ test("normal mode preserves previous guard behavior and named children do not inherit Auto",async t=>{
46
+ const h=harness(t);assert.equal((await h.call("bash",{command:"git push origin feature"})).block,true);assert.equal(h.requests.length,0);
47
+ await h.enable();await h.emit("before_agent_start",{agent:{name:"worker"},systemPromptOptions:{}});
48
+ assert.equal((await h.call("bash",{command:"git push origin feature"})).block,true);assert.equal(h.requests.length,0);
49
+ });