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
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, readFileSync, rmSync, symlinkSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import test from "node:test";
@@ -14,10 +14,14 @@ import {
14
14
  NativeReviewCliV216,
15
15
  NativeReviewIntegrationError,
16
16
  createNodeExecFileAdapter,
17
+ createWindowsProcessTreeKiller,
18
+ windowsTaskkillPath,
19
+ gentleAiProcessEnvironment,
17
20
  isNativeReviewUnachievableVerbRefused,
18
21
  type ExecFileAdapter,
19
22
  type NativeTargetStatusRequest,
20
23
  } from "../lib/native-review-cli.ts";
24
+ import { setBundledInstallForTesting } from "../lib/gentle-shell-bundled.ts";
21
25
 
22
26
  const fixture = (name: string): Record<string, unknown> => JSON.parse(
23
27
  readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", name), "utf8"),
@@ -418,6 +422,63 @@ test("native START queries STATUS then executes only the provider-rendered order
418
422
  ]);
419
423
  });
420
424
 
425
+ // Captured from the released gentle-ai 4.0.0 binary, which predates START
426
+ // lens selection (verify-always-rdd-high S8).
427
+ const LENS_SELECTION_UNSUPPORTED_FAILURE = {
428
+ schema: "gentle-ai.review-integration.failure/v2", contract: "gentle-ai.review-integration/v2", operation: "review.status",
429
+ phase: "preflight", code: "invalid_request", message: "The negotiated review request is invalid.", mutation_outcome: "not_started",
430
+ authority_applicability: "not_evaluated", retry_safe: true, replayability: "not_replayable", required_inputs: [],
431
+ next_action: "correct_request", cause: "flag provided but not defined: -lenses",
432
+ };
433
+
434
+ test("native START preflights the agent lens selection through STATUS", async () => {
435
+ const tokens = [
436
+ "--contract=gentle-ai.review-integration/v2", "--cwd=/repo", `--target=${TARGET}`,
437
+ "--projection=workspace", "--agent=pi", "--consent=relay",
438
+ "--lenses=review-risk,review-reliability", "--lenses-reason=changes token parsing",
439
+ ];
440
+ const queue = queuedAdapter([
441
+ { stdout: JSON.stringify(negotiatedStartStatus(TARGET, tokens)) },
442
+ { stdout: JSON.stringify(fixture("start-v3-zero-lens-closed.captured.json")) },
443
+ ]);
444
+ await client(queue.adapter).start({ cwd: "/repo", targetIdentity: TARGET, lenses: ["review-risk", "review-reliability"], lensesReason: "changes token parsing" });
445
+ assert.deepEqual(queue.calls.map((call) => call.arguments), [
446
+ ["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi",
447
+ "--lenses", "review-risk,review-reliability", "--lenses-reason", "changes token parsing", "--next-transition"],
448
+ ["review", "start", ...tokens],
449
+ ]);
450
+ });
451
+
452
+ test("native START falls back to the tier default when the provider predates lens selection", async () => {
453
+ const tokens = [
454
+ "--contract=gentle-ai.review-integration/v2", "--cwd=/repo", `--target=${TARGET}`,
455
+ "--projection=workspace", "--agent=pi", "--consent=relay",
456
+ ];
457
+ const queue = queuedAdapter([
458
+ { stdout: JSON.stringify(LENS_SELECTION_UNSUPPORTED_FAILURE), exitCode: 1 },
459
+ { stdout: JSON.stringify(negotiatedStartStatus(TARGET, tokens)) },
460
+ { stdout: JSON.stringify(fixture("start-v3-zero-lens-closed.captured.json")) },
461
+ ]);
462
+ const result = await client(queue.adapter).start({ cwd: "/repo", targetIdentity: TARGET, lenses: ["review-risk"], lensesReason: "changes token parsing" });
463
+ assert.equal(result.action, "closed");
464
+ assert.deepEqual(queue.calls.map((call) => call.arguments), [
465
+ ["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi",
466
+ "--lenses", "review-risk", "--lenses-reason", "changes token parsing", "--next-transition"],
467
+ ["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi", "--next-transition"],
468
+ ["review", "start", ...tokens],
469
+ ]);
470
+ });
471
+
472
+ test("native START never retries a selection refusal that is not an unknown lens flag", async () => {
473
+ const refusal = { ...LENS_SELECTION_UNSUPPORTED_FAILURE, cause: 'review status --lenses names unknown lens "security"' };
474
+ const queue = queuedAdapter([{ stdout: JSON.stringify(refusal), exitCode: 1 }]);
475
+ await assert.rejects(
476
+ () => client(queue.adapter).start({ cwd: "/repo", targetIdentity: TARGET, lenses: ["security"], lensesReason: "why" }),
477
+ (error: unknown) => error instanceof NativeReviewIntegrationError && error.failureEnvelope.cause === refusal.cause,
478
+ );
479
+ assert.equal(queue.calls.length, 1);
480
+ });
481
+
421
482
  test("native START relays provider-owned intended-untracked selection and transition argv verbatim", async () => {
422
483
  const selectionValue = JSON.stringify(["new.ts", "nested/other.ts"]);
423
484
  const selectionTokens = ["--selection-kind=untracked", "--selection-json={{value}}", "--selection-proof=provider-issued"];
@@ -495,6 +556,85 @@ test("native consent completion decodes START/v4 and replays its provider argv u
495
556
  assert.deepEqual(queue.calls[0]?.arguments, consent.choices[0].invocation.split(" ").slice(1));
496
557
  });
497
558
 
559
+ // Builds a Pi-bound consent/v3 envelope whose choices carry `--lenses-reason`
560
+ // rendered exactly as `rendered`, plus the argv Pi must replay for the granted
561
+ // choice when that rendering splits into `reason`.
562
+ function consentWithLensReason(rendered: string, reason: string): { consent: ReturnType<typeof decodeReviewConsentV3>; expected: readonly string[] } {
563
+ const rawConsent = fixture("consent-v3.captured.json");
564
+ rawConsent.agent = "pi";
565
+ const expected: string[][] = [];
566
+ for (const choice of rawConsent.choices as Record<string, unknown>[]) {
567
+ const bound = String(choice.invocation)
568
+ .replace(/--cwd \S+ --target/, "--cwd /repo --target")
569
+ .replace(" --consent ", " --agent pi --lenses-reason REASON --consent ");
570
+ choice.invocation = bound.replace("REASON", () => rendered);
571
+ expected.push(bound.split(" ").slice(1).map((word) => word === "REASON" ? reason : word));
572
+ }
573
+ const consent = decodeReviewConsentV3(rawConsent, "pi");
574
+ assert.ok(consent.choices[0].invocation.includes(` --lenses-reason ${rendered} `));
575
+ return { consent, expected: expected[0]! };
576
+ }
577
+
578
+ // Answers the granted choice and proves Pi launched the package binary exactly
579
+ // once with the exact argv, through execFile and never through a shell.
580
+ async function assertLensReasonReplay(rendered: string, reason: string): Promise<void> {
581
+ const { consent, expected } = consentWithLensReason(rendered, reason);
582
+ const queue = queuedAdapter([{ stdout: JSON.stringify(reviewingStartV4(consent.targetIdentity)) }]);
583
+ const answered = await client(queue.adapter).answerConsent({ cwd: "/repo", consent, answer: "granted" });
584
+ assert.equal(answered.kind, "started");
585
+ assert.equal(queue.calls.length, 1);
586
+ assert.equal(queue.calls[0]?.file, "/package/.gentle-ai/gentle-ai");
587
+ assert.deepEqual(queue.calls[0]?.arguments, expected);
588
+ const replayed = queue.calls[0]?.arguments ?? [];
589
+ const reasonIndex = replayed.indexOf("--lenses-reason");
590
+ assert.deepEqual(replayed.slice(reasonIndex, reasonIndex + 2), ["--lenses-reason", reason]);
591
+ }
592
+
593
+ // gentle-ai renders a lens reason with POSIX shell quoting, so an apostrophe
594
+ // arrives as `'...'\''...'`. Pi splits it into exact argv and never runs a shell.
595
+ test("native consent answer replays a POSIX-quoted apostrophe lens reason as one exact argument", async () => {
596
+ await assertLensReasonReplay("'touched the package'\\''s parser'", "touched the package's parser");
597
+ });
598
+
599
+ test("native consent answer treats a backslash before an apostrophe outside quotes as an escaped apostrophe", async () => {
600
+ await assertLensReasonReplay("package\\'s", "package's");
601
+ });
602
+
603
+ test("native consent answer treats a backslash before a double quote outside quotes as an escaped double quote", async () => {
604
+ await assertLensReasonReplay('say\\"x\\"', 'say"x"');
605
+ });
606
+
607
+ test("native consent answer joins a token across a backslash-escaped space outside quotes", async () => {
608
+ await assertLensReasonReplay("touched\\ parser", "touched parser");
609
+ });
610
+
611
+ test("native consent answer keeps a backslash before any other character verbatim outside quotes", async () => {
612
+ await assertLensReasonReplay("C:\\repo", "C:\\repo");
613
+ await assertLensReasonReplay("\\\\server\\share", "\\\\server\\share");
614
+ await assertLensReasonReplay("touched\\nparser", "touched\\nparser");
615
+ });
616
+
617
+ test("native consent answer keeps single quotes literal around double quotes and backslashes", async () => {
618
+ await assertLensReasonReplay("'say \"x\" and \\y'", 'say "x" and \\y');
619
+ await assertLensReasonReplay("'touched the package\\'", "touched the package\\");
620
+ });
621
+
622
+ test("native consent answer keeps backslashes verbatim inside double quotes", async () => {
623
+ await assertLensReasonReplay('"C:\\repo\\"', "C:\\repo\\");
624
+ });
625
+
626
+ test("native consent answer still rejects an unterminated quote before launching the provider", async () => {
627
+ for (const rendered of ["'touched the package'\\''s parser", "'touched the package", '"say x']) {
628
+ const { consent } = consentWithLensReason(rendered, "unused");
629
+ const queue = queuedAdapter([]);
630
+ await assert.rejects(
631
+ () => client(queue.adapter).answerConsent({ cwd: "/repo", consent, answer: "granted" }),
632
+ (error: unknown) => error instanceof TypeError && error.message === "Native consent invocation has invalid quoting",
633
+ );
634
+ assert.deepEqual(queue.calls, []);
635
+ }
636
+ });
637
+
498
638
  test("native START refuses invalid committed-range and target bindings before STATUS", async () => {
499
639
  for (const request of [
500
640
  { cwd: "/repo", baseRef: "origin/main" },
@@ -665,6 +805,76 @@ test("node execFile adapter passes AbortSignal to child_process", async () => {
665
805
  await assert.rejects(pending, (error: unknown) => error instanceof Error && error.name === "AbortError");
666
806
  });
667
807
 
808
+ // On Windows, killing gentle-ai alone leaves its git children running against
809
+ // the repository index; abort and timeout must take the whole tree down.
810
+ test("node execFile adapter kills the whole process tree on Windows abort and timeout", async () => {
811
+ const killed: number[] = [];
812
+ const killProcessTree = (pid: number) => { killed.push(pid); process.kill(pid); };
813
+ const adapter = createNodeExecFileAdapter({ platform: "win32", killProcessTree });
814
+ const request = { file: process.execPath, arguments: ["-e", "setTimeout(() => {}, 10_000)"], cwd: process.cwd(), maxBufferBytes: 1024 };
815
+
816
+ const controller = new AbortController();
817
+ const aborted = adapter({ ...request, timeoutMs: 30_000, signal: controller.signal });
818
+ setTimeout(() => controller.abort(), 50);
819
+ await assert.rejects(aborted, (error: unknown) => error instanceof Error && error.name === "AbortError");
820
+ assert.equal(killed.length, 1, "abort kills the tree once");
821
+
822
+ const timedOut = await adapter({ ...request, timeoutMs: 50 });
823
+ assert.equal(timedOut.timedOut, true);
824
+ assert.equal(killed.length, 2, "timeout kills the tree once");
825
+
826
+ const preAborted = new AbortController();
827
+ preAborted.abort();
828
+ await assert.rejects(adapter({ ...request, timeoutMs: 30_000, signal: preAborted.signal }), (error: unknown) => error instanceof Error && error.name === "AbortError");
829
+ assert.equal(killed.length, 2, "an already-aborted call spawns nothing");
830
+
831
+ const finished = await adapter({ ...request, arguments: ["-p", "'done'"], timeoutMs: 30_000 });
832
+ assert.deepEqual([finished.exitCode, finished.stdout.trim(), finished.timedOut], [0, "done", false]);
833
+ assert.equal(killed.length, 2, "a call that finishes kills nothing");
834
+ });
835
+
836
+ // taskkill can be unavailable (AppLocker, a broken PATH): the deadline must
837
+ // still hold, through a direct kill of gentle-ai itself.
838
+ test("node execFile adapter still enforces the timeout on Windows when the tree kill does nothing", async () => {
839
+ const adapter = createNodeExecFileAdapter({ platform: "win32", killProcessTree: () => {} });
840
+ const startedAt = Date.now();
841
+ const result = await adapter({ file: process.execPath, arguments: ["-e", "setTimeout(() => {}, 5_000)"], cwd: process.cwd(), maxBufferBytes: 1024, timeoutMs: 100 });
842
+ assert.equal(result.timedOut, true);
843
+ assert.ok(Date.now() - startedAt < 3_000, `settled after ${Date.now() - startedAt}ms`);
844
+ });
845
+
846
+ // A descendant that inherited gentle-ai's stdout/stderr keeps the pipes open
847
+ // after gentle-ai itself exits; the deadline must still hold.
848
+ test("node execFile adapter enforces the Windows timeout when a descendant holds the inherited pipes", async () => {
849
+ const directory = mkdtempSync(join(tmpdir(), "gentle-pi-tree-kill-"));
850
+ const pidFile = join(directory, "descendant.pid");
851
+ const script = `const { spawn } = require("node:child_process"); const d = spawn(process.execPath, ["-e", "setTimeout(() => {}, 30000)"], { stdio: "inherit" }); require("node:fs").writeFileSync(${JSON.stringify(pidFile)}, String(d.pid)); d.unref();`;
852
+ try {
853
+ const adapter = createNodeExecFileAdapter({ platform: "win32", killProcessTree: () => {} });
854
+ const startedAt = Date.now();
855
+ const pending = adapter({ file: process.execPath, arguments: ["-e", script], cwd: process.cwd(), maxBufferBytes: 1024, timeoutMs: 300 });
856
+ const result = await Promise.race([pending, new Promise<"unsettled">((resolve) => setTimeout(() => resolve("unsettled"), 6_000).unref())]);
857
+ assert.notEqual(result, "unsettled", "the call settles while the descendant still holds the pipes");
858
+ if (result === "unsettled") return;
859
+ assert.equal(result.timedOut, true);
860
+ assert.ok(Date.now() - startedAt < 4_000, `settled after ${Date.now() - startedAt}ms`);
861
+ } finally {
862
+ try { process.kill(Number(readFileSync(pidFile, "utf8"))); } catch { /* Already gone. */ }
863
+ rmSync(directory, { recursive: true, force: true });
864
+ }
865
+ });
866
+
867
+ // A bare "taskkill" resolves from the current directory first on Windows, so a
868
+ // repository shipping its own taskkill.exe would run on any abort or timeout.
869
+ test("Windows tree kill runs taskkill by its absolute System32 path from a trusted cwd", () => {
870
+ const calls: Array<{ file: string; args: readonly string[]; cwd: string }> = [];
871
+ const kill = createWindowsProcessTreeKiller({ SystemRoot: "D:\\Windows" }, (file, args, options) => { calls.push({ file, args, cwd: options.cwd }); return { on() {} }; });
872
+ kill(42);
873
+ assert.deepEqual(calls, [{ file: "D:\\Windows\\System32\\taskkill.exe", args: ["/pid", "42", "/T", "/F"], cwd: "D:\\Windows\\System32" }]);
874
+ assert.equal(windowsTaskkillPath({}), "C:\\Windows\\System32\\taskkill.exe");
875
+ assert.equal(windowsTaskkillPath({ SystemRoot: "relative\\dir" }), "C:\\Windows\\System32\\taskkill.exe", "a relative SystemRoot is never trusted");
876
+ });
877
+
668
878
  test("capture-result receives the controller AbortSignal without an automatic mutation timeout", async () => {
669
879
  const controller = new AbortController();
670
880
  controller.abort();
@@ -909,3 +1119,28 @@ test("acknowledge-approved keeps its fail-closed stderr and typed refusal discip
909
1119
  (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT,
910
1120
  );
911
1121
  });
1122
+
1123
+ // S9: this package running from a bundled install (the testing overlay stands
1124
+ // in for its location; the detection itself is tested with real installs in
1125
+ // gentle-ai-binary-isolation.test.ts) and the launcher's GENTLE_SHELL_PREFIX.
1126
+ function bundledIsolation(t: { after: (fn: () => void) => void }): { prefix: string; env: NodeJS.ProcessEnv } {
1127
+ const prefix = realpathSync(mkdtempSync(join(tmpdir(), "gentle-ai-isolation-site-")));
1128
+ setBundledInstallForTesting({ root: prefix });
1129
+ t.after(() => {
1130
+ setBundledInstallForTesting(undefined);
1131
+ rmSync(prefix, { recursive: true, force: true });
1132
+ });
1133
+ return { prefix, env: { PATH: "/usr/bin", GENTLE_SHELL_PREFIX: prefix } };
1134
+ }
1135
+
1136
+ test("the central gentle-ai environment carries Gentle AI's isolation variables only in a bundled install", (t) => {
1137
+ const { prefix, env } = bundledIsolation(t);
1138
+ const bundled = gentleAiProcessEnvironment(env);
1139
+ assert.equal(bundled.GENTLE_AI_HOME, join(prefix, "gentle-ai"));
1140
+ assert.equal(bundled.GENTLE_AI_PI_CONFIG_DIR, join(prefix, "config", "gentle-ai"));
1141
+ assert.equal(bundled.GENTLE_PI_CONFIG_HOME, join(prefix, "config"), "the parent Gentle AI 4.0.0 reads it as");
1142
+ assert.equal(bundled.GENTLE_AI_NO_SELF_UPDATE, "1");
1143
+ assert.equal(bundled.GENTLE_PI_REVIEW_RELAY_CONTRACT, "gentle-pi.review-relay/v1");
1144
+ const ordinary = gentleAiProcessEnvironment({ PATH: "/usr/bin" });
1145
+ assert.deepEqual(ordinary, { PATH: "/usr/bin", GENTLE_PI_REVIEW_RELAY_CONTRACT: "gentle-pi.review-relay/v1" });
1146
+ });
@@ -87,11 +87,11 @@ function queuedAdapter(outputs: readonly Record<string, unknown>[]): { adapter:
87
87
  }
88
88
 
89
89
  function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
90
- return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
90
+ return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined);
91
91
  }
92
92
 
93
93
  function runtimeClient(adapter: ExecFileAdapter): RuntimeNativeReviewCliV216 {
94
- return new RuntimeNativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
94
+ return new RuntimeNativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined);
95
95
  }
96
96
 
97
97
  test("negotiated ordinary START executes the complete STATUS-rendered relay vector and preserves the consent envelope", async () => {
@@ -44,7 +44,7 @@ const resolvedBinary = (() => {
44
44
  }
45
45
  })();
46
46
  const nativeBinaryGate = requireNativeBinary({ resolvedBinary, digestsPinned: true, env: process.env });
47
- if (!nativeBinaryGate.run) console.log(`native-review-parity-runtime: ${nativeBinaryGate.reason}`);
47
+ if (nativeBinaryGate.run === false) console.log(`native-review-parity-runtime: ${nativeBinaryGate.reason}`);
48
48
  const test = nativeBinaryGate.run ? baseTest : baseTest.skip;
49
49
  const binary = resolvedBinary ?? "";
50
50
 
@@ -181,8 +181,6 @@ interface ParityRuntime {
181
181
  interface ParityRuntimeOptions {
182
182
  candidateViews?: CandidateViewRegistry;
183
183
  pendingReviewConsentRegistry?: PendingReviewConsentRegistry;
184
- now?: () => number;
185
- scheduleTimer?: (callback: () => void, delayMs: number) => { unref: () => void };
186
184
  }
187
185
 
188
186
  function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityRuntimeOptions = {}): ParityRuntime {
@@ -193,8 +191,6 @@ function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityR
193
191
  nativeReviewCli,
194
192
  candidateViews: options.candidateViews ?? new CandidateViewRegistry(),
195
193
  pendingReviewConsentRegistry: options.pendingReviewConsentRegistry ?? new PendingReviewConsentRegistry(),
196
- now: options.now,
197
- scheduleTimer: options.scheduleTimer,
198
194
  } as unknown as Parameters<typeof createGentleAiExtension>[0];
199
195
  __testing.createGentleAiExtension(dependencies)({
200
196
  on(name: string, handler: RegisteredEvent) { events.set(name, handler); },
@@ -330,7 +326,7 @@ async function answerConsent(runtime: ParityRuntime, cwd: string, binding: unkno
330
326
  // that names itself and its exit. It still carries the current negotiated
331
327
  // STATUS as reconciliation context, but it never reads as a healthy
332
328
  // pre-start "ready".
333
- function assertStaleConsentBinding(outcome: Record<string, unknown>, binding: unknown, code: "consent-binding-unknown" | "consent-binding-expired" | "consent-binding-already-consumed"): void {
329
+ function assertStaleConsentBinding(outcome: Record<string, unknown>, binding: unknown, code: "consent-binding-unknown" | "consent-binding-already-consumed"): void {
334
330
  assert.equal(outcome.status, "blocked");
335
331
  assert.equal(outcome.outcome, "consent-binding-stale");
336
332
  assert.equal(outcome.consent_binding, binding);
@@ -677,7 +673,7 @@ test("unavailable and ambiguous consent follow-ups never replay a consumed bindi
677
673
  assert.equal(statusCalls, callsBeforeStaleReconciliation + 1, "already-consumed binding reconciliation performs exactly one additional STATUS call");
678
674
  });
679
675
 
680
- test("concurrent answers atomically claim consent before review-mode gating and ignore a queued expiry callback", async (t) => {
676
+ test("concurrent answers atomically claim consent before review-mode gating", async (t) => {
681
677
  const cwd = repository(t);
682
678
  const fixture = consentNative(cwd);
683
679
  const candidateViews = new CandidateViewRegistry();
@@ -687,7 +683,6 @@ test("concurrent answers atomically claim consent before review-mode gating and
687
683
  cleanupCalls += 1;
688
684
  cleanup(token);
689
685
  };
690
- const scheduled: Array<() => void> = [];
691
686
  let releaseModeGate: (() => void) | undefined;
692
687
  const modeGate = new Promise<void>((resolve) => { releaseModeGate = resolve; });
693
688
  let modeCalls = 0;
@@ -702,13 +697,12 @@ test("concurrent answers atomically claim consent before review-mode gating and
702
697
  let nativeAnswerCalls = 0;
703
698
  fixture.native.answerConsent = async (request) => {
704
699
  nativeAnswerCalls += 1;
705
- assert.equal(cleanupCalls, 0, "a queued expiry callback must not clean claimed candidate authority before the provider answer");
700
+ assert.equal(cleanupCalls, 0, "candidate authority remains live before the provider answer");
706
701
  await providerGate;
707
702
  return await nativeAnswer(request);
708
703
  };
709
704
  const runtime = parityRuntime(fixture.native, {
710
705
  candidateViews,
711
- scheduleTimer: (callback) => { scheduled.push(callback); return { unref() {} }; },
712
706
  });
713
707
  const blocked = await beginConsent(runtime, cwd);
714
708
  fixture.native.reviewMode = async () => {
@@ -726,9 +720,7 @@ test("concurrent answers atomically claim consent before review-mode gating and
726
720
  assert.equal(modeCalls, 1, "the first answer pauses at the asynchronous review-mode gate");
727
721
  const second = answerConsent(runtime, cwd, blocked.consent_binding, "declined");
728
722
  await new Promise<void>((resolve) => setImmediate(resolve));
729
- assert.equal(scheduled.length, 1);
730
- scheduled[0]!();
731
- assert.equal(cleanupCalls, 0, "a queued expiry callback is a harmless no-op after the answer claim");
723
+ assert.equal(cleanupCalls, 0, "claimed consent retains candidate authority until the answer settles");
732
724
 
733
725
  releaseModeGate!();
734
726
  releaseProvider!();
@@ -741,47 +733,19 @@ test("concurrent answers atomically claim consent before review-mode gating and
741
733
  assert.equal(cleanupCalls, 1, "only the answered binding cleans candidate authority");
742
734
  });
743
735
 
744
- test("timer-cleaned and synchronously pruned consent bindings remain expired within their session", async (t) => {
736
+ test("unanswered consent remains reusable after the former deadline and answers only once", async (t) => {
745
737
  const cwd = repository(t);
746
738
  const fixture = consentNative(cwd);
747
- const registry = new PendingReviewConsentRegistry();
748
- const start = 1_000;
749
- let now = start;
750
- const scheduled: Array<() => void> = [];
751
- const runtime = parityRuntime(fixture.native, {
752
- pendingReviewConsentRegistry: registry,
753
- now: () => now,
754
- scheduleTimer: (callback) => { scheduled.push(callback); return { unref() {} }; },
755
- });
739
+ const runtime = parityRuntime(fixture.native);
740
+ let now = 1_000;
741
+ t.mock.method(Date, "now", () => now);
756
742
  const first = await beginConsent(runtime, cwd);
743
+ now += 24 * 60 * 60 * 1000;
757
744
  const reused = await beginConsent(runtime, cwd);
758
745
  assert.equal(reused.consent_binding, first.consent_binding);
759
- assert.equal(scheduled.length, 1);
760
- now += 10 * 60 * 1000;
761
- // The real TTL callback drops the frozen candidate immediately. Its
762
- // session-local disposition remains only to classify this stale answer.
763
- scheduled[0]!();
764
- const timerExpired = await answerConsent(runtime, cwd, first.consent_binding, "declined");
765
- assert.equal(timerExpired.operation, "answer-consent");
766
- assertStaleConsentBinding(timerExpired, first.consent_binding, "consent-binding-expired");
767
- assert.match(String((timerExpired.diagnostics as { message?: unknown }).message), /expired after 10 minutes/);
768
- const second = await beginConsent(runtime, cwd);
769
- assert.notEqual(second.consent_binding, first.consent_binding);
770
- assert.equal(scheduled.length, 2);
771
- assertStaleConsentBinding(await answerConsent(runtime, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-expired");
772
-
773
- // No second callback runs: START synchronously prunes this unused binding
774
- // before it can reuse the frozen candidate, and its answer stays typed expiry.
775
- now += 10 * 60 * 1000;
776
- const third = await beginConsent(runtime, cwd);
777
- assert.notEqual(third.consent_binding, second.consent_binding);
778
- assertStaleConsentBinding(await answerConsent(runtime, cwd, second.consent_binding, "declined"), second.consent_binding, "consent-binding-expired");
779
-
780
- const reloaded = parityRuntime(fixture.native, { pendingReviewConsentRegistry: new PendingReviewConsentRegistry() });
781
- assertStaleConsentBinding(await answerConsent(reloaded, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-unknown");
782
- const afterReload = await beginConsent(reloaded, cwd);
783
- assert.notEqual(afterReload.consent_binding, third.consent_binding);
784
- assert.equal(fixture.starts.count, 5);
746
+ const answered = await answerConsent(runtime, cwd, first.consent_binding, "declined");
747
+ assert.equal(answered.outcome, "consent-declined-this-candidate");
748
+ assertStaleConsentBinding(await answerConsent(runtime, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-already-consumed");
785
749
  });
786
750
 
787
751
  test("native START maps only provider facts and omits absent evidence", async (t) => {
@@ -89,3 +89,24 @@ test("an empty file is named before it is described", () => {
89
89
  test("a reason with no path contributes no phrase", () => {
90
90
  assert.deepEqual(nativeRiskEvidencePhrases("high", [{ code: "empty_content" }]), []);
91
91
  });
92
+
93
+ // Field truth from gentle-ai main (4.1.0-dev): one committed high-risk
94
+ // candidate's consent/v3 `risk_evidence` beside its START/v4 `risk_reasons`.
95
+ test("dangerous sink and agent escalation reasons render the gentle-ai phrases", () => {
96
+ assert.deepEqual(
97
+ nativeRiskEvidencePhrases("high", [
98
+ { code: "dangerous_sink", signal: "dangerous_sink", path: "auth/session.py" },
99
+ { code: "hot_path", signal: "auth", path: "auth/session.py" },
100
+ { code: "process_boundary", signal: "shell_process", path: "auth/session.py" },
101
+ ]),
102
+ [
103
+ "a dangerous code pattern in auth/session.py",
104
+ "authentication in auth/session.py",
105
+ "code that starts other processes in auth/session.py",
106
+ ],
107
+ );
108
+ assert.deepEqual(
109
+ nativeRiskEvidencePhrases("high", [{ code: "agent_escalation", signal: "agent_escalation" }]),
110
+ ["the agent that made this change flagged it as high risk"],
111
+ );
112
+ });
@@ -0,0 +1,87 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ // gentle-shell#1731 T13 (evidence L24): in bench task x4 the user wrote "I may
7
+ // stop you between the two and pick the second part up later"; every Gentle
8
+ // cell delivered only the first part and paused by itself. Hedged delivery
9
+ // wording asks for resumable notes and separate commits, never a stop, and
10
+ // the close step must not leave tracked tasks pending without a user stop.
11
+
12
+ const REPO_ROOT = join(import.meta.dirname, "..");
13
+ const read = (relative: string): string => readFileSync(join(REPO_ROOT, relative), "utf8");
14
+
15
+ const tracking = read("assets/orchestrator-tracking.md");
16
+ const core = read("assets/orchestrator.md");
17
+
18
+ function authorizationSection(): string {
19
+ const start = tracking.indexOf("#### Authorization and progress");
20
+ assert.ok(start !== -1, "tracking module must keep its Authorization and progress section");
21
+ const next = tracking.indexOf("\n#### ", start + 1);
22
+ return tracking.slice(start, next === -1 ? undefined : next);
23
+ }
24
+
25
+ test("T13: conditional change intent is narrowed to whether a change is authorized at all", () => {
26
+ const section = authorizationSection();
27
+ assert.ok(
28
+ section.includes(
29
+ "Ambiguous or conditional change intent (unclear whether a change is authorized at all) requires one clarification; stop and wait.",
30
+ ),
31
+ "the clarification clause must be scoped to authorization itself",
32
+ );
33
+ assert.ok(
34
+ !section.includes("Ambiguous or conditional change intent requires one clarification"),
35
+ "the unscoped clarification clause must be gone",
36
+ );
37
+ });
38
+
39
+ test("T13: hedged delivery wording asks for resumable notes and separate commits, not a stop", () => {
40
+ const section = authorizationSection();
41
+ for (const clause of [
42
+ "Hedged delivery wording (the user may stop you, review commits one at a time, or pick the work up later)",
43
+ "asks for resumable notes and separate commits, not a stop",
44
+ "complete every authorized task in the current turn unless the user explicitly says to wait",
45
+ ]) {
46
+ assert.ok(section.includes(clause), `tracking module is missing: ${clause}`);
47
+ }
48
+ const hedge = section.slice(section.indexOf("Hedged delivery wording"));
49
+ const sentence = hedge.slice(0, hedge.indexOf(". ") + 1);
50
+ assert.doesNotMatch(sentence, /\bask (?:one|a|the user)\b|question/i, "the hedge rule must not add a product or design question");
51
+ });
52
+
53
+ test("T13: the Close gate requires continuing or quoting the user's explicit stop", () => {
54
+ const section = authorizationSection();
55
+ assert.ok(
56
+ section.includes(
57
+ "Close gate: before the final answer, if any tracked task in the feature document or `todo` is still pending, continue it or quote the exact user sentence that orders the stop.",
58
+ ),
59
+ "tracking module must carry the Close gate",
60
+ );
61
+ });
62
+
63
+ test("T13: budgets hold and the pinned human-control line is untouched", () => {
64
+ const bytes = Buffer.byteLength(tracking, "utf8");
65
+ assert.ok(bytes <= 12_600, `orchestrator-tracking.md is ${bytes} B, over its 12,600 B budget`);
66
+ assert.ok(core.includes("- Preserve human control: user decisions beat agent momentum."), "core human-control line must stay verbatim");
67
+ assert.ok(core.includes("4. **Track**") && core.includes("`orchestrator-tracking.md`"), "Track must still load the tracking module");
68
+ });
69
+
70
+ test("T13: the always-on harness narrows conditional intent and gates Close on pending tasks", () => {
71
+ // The harness ODD steps are injected on every request, including the small
72
+ // path that never loads the tracking module, so the narrowing must live there too.
73
+ const harness = read("extensions/gentle-ai.ts");
74
+ assert.ok(
75
+ harness.includes("Ambiguous or conditional change intent (unclear whether a change is authorized at all) gets one clarification; stop and wait."),
76
+ "harness Authorize step must narrow conditional intent",
77
+ );
78
+ assert.ok(!harness.includes("Ambiguous or conditional change intent gets one clarification"), "the broad harness wording must be gone");
79
+ assert.ok(
80
+ harness.includes("A user saying they may stop you or resume later asks for notes and separate commits, not a stop."),
81
+ "harness must say hedged delivery wording is not a stop",
82
+ );
83
+ assert.ok(
84
+ harness.includes("Never end with a tracked task pending unless you quote the user's explicit stop."),
85
+ "harness Close step must gate pending tracked tasks",
86
+ );
87
+ });