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,14 +1,17 @@
1
1
  import { isSessionChangeEvidence, type SessionChangeEvidence } from "./session-changes.ts";
2
- import { chmodSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { Duplex, Readable, Writable } from "node:stream";
2
+ import { chmodSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
4
+ import { tmpdir, userInfo } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { Duplex, type Readable, type Writable } from "node:stream";
6
7
  import { stripVTControlCharacters } from "node:util";
7
8
  import { withoutInteractiveHost } from "./rpc-host.ts";
9
+ import { childPackageExtensionArgs } from "./child-package-injection.ts";
8
10
  import { AGENT_MODE, formatModelRef, type AgentDefinition, type AgentMode, type ModelRef } from "./agents-config.ts";
9
11
  import { CHILD_QUERY_MAX_INFLIGHT, CHILD_QUERY_TIMEOUT_MS, parseChildFrame, validChildMessage, validChildQueryId } from "./agents-messaging.ts";
10
12
  import { ParentStandingReviewPermissionBroker } from "./review-session-standing-permission-ipc.ts";
11
13
  import { isFinished, normalizeRpcEvent, ToolArgumentProgress, TASK_EVENT, TASK_STATUS, taskLabel, type AskRequest, type ChildResponseObservation, type TaskRecord, type TaskStore } from "./agents-protocol.ts";
14
+ import { WriterSurfaceRegistry, writerSurfaceConflictMessage } from "./writer-surfaces.ts";
12
15
 
13
16
  // Gentle Agents runner. Every subagent is its own `pi --mode rpc` process:
14
17
  // the host never runs subagent work on the TUI thread. It writes JSON
@@ -16,12 +19,12 @@ import { isFinished, normalizeRpcEvent, ToolArgumentProgress, TASK_EVENT, TASK_S
16
19
  // and enforces an inactivity watchdog per task.
17
20
 
18
21
  export interface ChildLike {
19
- pid: number | undefined;
22
+ pid?: number;
20
23
  connected?: boolean;
21
- stdin: Writable;
22
- stdout: Readable;
24
+ stdin: Writable | null;
25
+ stdout: Readable | null;
23
26
  stderr: Readable | null | undefined;
24
- stdio?: Array<Duplex | null | undefined>;
27
+ stdio?: Array<Readable | Writable | null | undefined>;
25
28
  kill(signal?: NodeJS.Signals): boolean;
26
29
  send?(message: Record<string, unknown>, callback?: (error: Error | null) => void): boolean;
27
30
  disconnect?(): void;
@@ -35,6 +38,7 @@ export interface SpawnOptions {
35
38
  cwd: string;
36
39
  env: NodeJS.ProcessEnv;
37
40
  detached?: boolean;
41
+ windowsHide?: boolean;
38
42
  stdio?: Array<"pipe" | "ignore" | "inherit" | "ipc" | "overlapped">;
39
43
  }
40
44
 
@@ -126,8 +130,18 @@ export interface TaskRequest {
126
130
  sessionDir: string;
127
131
  resumeSessionPath: string | undefined;
128
132
  env: NodeJS.ProcessEnv;
129
- // Untrusted narrowing intent; paths come only from matching host provenance.
133
+ // Host-provided only: the launcher's package injection signal (#1690) or
134
+ // the curated fallback. Never derived from tool input or agent definitions.
130
135
  extensionPaths?: string[];
136
+ // Same host-only provenance as extensionPaths (#1690).
137
+ noExtensions?: boolean;
138
+ // Parsed `## Allowed edit surfaces` of a bounded writer. While the task is
139
+ // queued or running it claims them in `cwd`; run() rejects an overlapping
140
+ // claim (gentle-shell#1731). Read-only agents leave this unset.
141
+ writerSurfaces?: readonly string[];
142
+ // Canonical worktree root the claim is keyed to; the parent computes it before
143
+ // run() so admission stays synchronous. Defaults to `cwd`.
144
+ writerRoot?: string;
131
145
  // Synchronous admission recheck at dequeue, before any OS spawn. Throws fail
132
146
  // only this task; unlike onLaunch, it must never persist Changes evidence.
133
147
  beforeSpawn?: () => void;
@@ -216,6 +230,8 @@ const MAX_TRANSPORT_PREFIX_CHARS = 64;
216
230
  const CHILD_MARKER = "GENTLE_PI_AGENTS_CHILD";
217
231
  const IPC_MARKER = "GENTLE_PI_AGENTS_OWNED_IPC";
218
232
  const PARENT_NOTIFICATION_TOOL = "subagent_parent_message";
233
+ /** The exact --tools list, so the child can report requested names Pi dropped silently (#1690). */
234
+ export const REQUESTED_TOOLS_ENV = "GENTLE_PI_AGENTS_REQUESTED_TOOLS";
219
235
  const DEFAULT_TOOLS: readonly string[] = [];
220
236
  const TERMINATION_GRACE_MS = 250;
221
237
  const GROUP_CONFIRM_MS = 25;
@@ -255,14 +271,53 @@ export function formatChildExit(code: number | null | undefined, signal?: string
255
271
 
256
272
  const hostProcess: ProcessControl = { platform: process.platform, kill: (pid, signal) => process.kill(pid, signal) };
257
273
 
274
+ // The --tools value, or undefined when Pi keeps its default tools.
275
+ function requestedTools(request: TaskRequest): string | undefined {
276
+ const tools = request.agent.tools.length > 0 ? [...new Set([...request.agent.tools, PARENT_NOTIFICATION_TOOL])] : DEFAULT_TOOLS;
277
+ return tools.length > 0 ? tools.join(",") : undefined;
278
+ }
279
+
280
+ const WEB_TOOL_NAMES = ["web_enable", "web_search", "source_check", "fetch_content", "get_search_content"];
281
+
282
+ /** Resolve one already installed, explicitly requested package. Pi's own loader
283
+ * follows its manifest and supplies SDK aliases; no install or ambient discovery. */
284
+ function requestedWebPackagePaths(request: TaskRequest): string[] {
285
+ if (!WEB_TOOL_NAMES.every(name => request.agent.tools.includes(name))) return [];
286
+ try {
287
+ let agentDir = request.env.PI_CODING_AGENT_DIR;
288
+ if (!agentDir || /^~(?:$|[/\\])/.test(agentDir)) {
289
+ const home = (process.platform === "win32" ? request.env.USERPROFILE : request.env.HOME) || userInfo().homedir;
290
+ agentDir = agentDir ? agentDir.replace(/^~/, home) : join(home, ".pi", "agent");
291
+ }
292
+ const manager = new DefaultPackageManager({
293
+ cwd: request.cwd,
294
+ agentDir,
295
+ settingsManager: SettingsManager.inMemory(),
296
+ });
297
+ const installed = manager.getInstalledPath("npm:pi-web-access", "user");
298
+ return installed ? [realpathSync(installed)] : [];
299
+ } catch {
300
+ // Missing/incompatible package remains an observable child capability gap.
301
+ return [];
302
+ }
303
+ }
304
+
258
305
  export function childArguments(request: TaskRequest, instructionsPath?: string): string[] {
259
306
  const args = ["--mode", "rpc", "--session-dir", request.sessionDir];
260
- for (const path of request.extensionPaths ?? []) args.push("--extension", path);
307
+ const extensionPaths = [...new Set(request.extensionPaths ?? [])];
308
+ for (const packagePath of requestedWebPackagePaths(request)) {
309
+ const injected = extensionPaths.some(path => {
310
+ try { return realpathSync(resolve(request.cwd, path)) === packagePath; }
311
+ catch { return false; }
312
+ });
313
+ if (!injected) extensionPaths.push(packagePath);
314
+ }
315
+ args.push(...childPackageExtensionArgs({ noExtensions: request.noExtensions === true, extensionPaths }));
261
316
  if (request.resumeSessionPath) args.push("--session", request.resumeSessionPath);
262
317
  if (request.model) args.push("--model", request.thinking ? `${formatModelRef(request.model)}:${request.thinking}` : formatModelRef(request.model));
263
318
  else if (request.thinking) args.push("--thinking", request.thinking);
264
- const tools = request.agent.tools.length > 0 ? [...new Set([...request.agent.tools, PARENT_NOTIFICATION_TOOL])] : DEFAULT_TOOLS;
265
- if (tools.length > 0) args.push("--tools", tools.join(","));
319
+ const tools = requestedTools(request);
320
+ if (tools !== undefined) args.push("--tools", tools);
266
321
  if (instructionsPath) {
267
322
  args.push("--append-system-prompt", instructionsPath);
268
323
  } else if (request.agent.instructions.length > 0) {
@@ -325,6 +380,7 @@ export class AgentRunner {
325
380
  private readonly waiters = new Map<string, Array<(task: TaskRecord) => void>>();
326
381
  private readonly queryWaiters = new Map<string, Array<(query: TaskQuery | undefined) => void>>();
327
382
  private readonly firstQueries = new Map<string, TaskQuery>();
383
+ private readonly writers = new WriterSurfaceRegistry();
328
384
  private counter = 0;
329
385
 
330
386
  constructor(store: TaskStore, limits: RunnerLimits, deps: RunnerDeps, hooks: RunnerHooks) {
@@ -366,8 +422,15 @@ export class AgentRunner {
366
422
  return task;
367
423
  }
368
424
 
425
+ // Check and claim happen in this one synchronous call, so two launches can
426
+ // never both pass admission for overlapping surfaces; finish() releases.
369
427
  run(request: TaskRequest): TaskRecord {
428
+ if (request.writerSurfaces) {
429
+ const conflicts = this.writers.conflicts(request.writerRoot ?? request.cwd, request.writerSurfaces);
430
+ if (conflicts.length) throw new Error(writerSurfaceConflictMessage(conflicts));
431
+ }
370
432
  const task = this.createTask(request);
433
+ if (request.writerSurfaces) this.writers.claim(task.id, request.writerRoot ?? request.cwd, request.writerSurfaces);
371
434
  this.queue.push({ task, request });
372
435
  queueMicrotask(() => this.pump());
373
436
  return task;
@@ -477,6 +540,9 @@ export class AgentRunner {
477
540
  // Do not forward stale legacy child selection or authorization.
478
541
  delete env.GENTLE_PI_SDD_REMEDIATION_PLAN;
479
542
  delete env.GENTLE_PI_RESEARCH_SELECTION;
543
+ const tools = requestedTools(request);
544
+ if (tools !== undefined) env[REQUESTED_TOOLS_ENV] = tools;
545
+ else delete env[REQUESTED_TOOLS_ENV];
480
546
  let instructionsTransportDir: string | undefined;
481
547
  let instructionsTransportPath: string | undefined;
482
548
  if (Buffer.byteLength(request.agent.instructions, "utf8") > MAX_INLINE_INSTRUCTIONS_BYTES) {
@@ -503,6 +569,7 @@ export class AgentRunner {
503
569
  cwd: request.cwd,
504
570
  env,
505
571
  detached,
572
+ windowsHide: true,
506
573
  stdio: hasParentPermissionChannel ? ["pipe", "pipe", "pipe", permissionChannelStdio, "ipc"] : ["pipe", "pipe", "pipe", "ipc"],
507
574
  });
508
575
  } catch (error) {
@@ -528,7 +595,7 @@ export class AgentRunner {
528
595
  }
529
596
  this.live.set(id, live);
530
597
  const permissionPipe = child.stdio?.[3];
531
- if (hasParentPermissionChannel && permissionPipe !== undefined && permissionPipe !== null) {
598
+ if (hasParentPermissionChannel && permissionPipe instanceof Duplex) {
532
599
  live.permissionBroker = new ParentStandingReviewPermissionBroker(
533
600
  { readable: permissionPipe, writable: permissionPipe },
534
601
  (repositoryIdentity) => this.live.get(id) === live && !live.terminal && request.authorizeParentStandingReviewPermission?.(repositoryIdentity) === true,
@@ -546,6 +613,11 @@ export class AgentRunner {
546
613
  try { request.onLaunch?.(); }
547
614
  catch (error) { this.requestStop(id, TASK_STATUS.FAILED, `could not register launched worktree: ${error instanceof Error ? error.message : String(error)}`); }
548
615
  });
616
+ child.on("exit", (code, signal) => this.exited(id, code, signal));
617
+ if (!child.stdin || !child.stdout) {
618
+ this.requestStop(id, TASK_STATUS.FAILED, "could not start pi: missing RPC streams");
619
+ return;
620
+ }
549
621
  child.stdin.on("error", () => {});
550
622
  this.armStall(id, live);
551
623
  const lines = new JsonLines((value) => this.receive(id, request, value));
@@ -556,7 +628,6 @@ export class AgentRunner {
556
628
  const tail = live.stderrTail + chunk;
557
629
  live.stderrTail = tail.length > STDERR_TAIL_MAX ? tail.slice(-STDERR_TAIL_MAX) : tail;
558
630
  });
559
- child.on("exit", (code, signal) => this.exited(id, code, signal));
560
631
  void this.send(id, { type: "get_state" }).then((response) => {
561
632
  const data = response.data as { sessionFile?: unknown; model?: { provider?: unknown; id?: unknown } | null; thinkingLevel?: unknown } | undefined;
562
633
  if (response.success !== true || live.terminal || this.live.get(id) !== live || !data) return;
@@ -726,7 +797,7 @@ export class AgentRunner {
726
797
 
727
798
  private write(live: LiveTask, payload: Record<string, unknown>): void {
728
799
  try {
729
- live.child.stdin.write(`${JSON.stringify(payload)}\n`);
800
+ live.child.stdin?.write(`${JSON.stringify(payload)}\n`);
730
801
  } catch {
731
802
  // the child is gone; the exit handler settles the task
732
803
  }
@@ -969,6 +1040,7 @@ export class AgentRunner {
969
1040
  this.live.delete(id);
970
1041
  // Quarantine already notified completion, but its retained slot is now free.
971
1042
  if (live.quarantined) {
1043
+ this.writers.release(id);
972
1044
  queueMicrotask(() => this.pump());
973
1045
  return;
974
1046
  }
@@ -978,6 +1050,9 @@ export class AgentRunner {
978
1050
  }
979
1051
 
980
1052
  private finish(id: string, status: TaskRecord["status"], error: string | null, live?: LiveTask): void {
1053
+ // A quarantined child may still be writing: its claim lives as long as its
1054
+ // reserved slot and is released in completeExit on proven exit.
1055
+ if (!live?.quarantined) this.writers.release(id);
981
1056
  const current = this.store.get(id);
982
1057
  if (!current || isFinished(current.status)) return;
983
1058
  const finished = this.store.update(id, { status, endedAt: this.deps.now(), error, lastStep: error ?? "done" });
@@ -0,0 +1,133 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { QuestionParams } from "./questionnaire/schema.ts";
3
+ import type { AnswerRow } from "./questionnaire/questionnaire-view.ts";
4
+ import { isInteractiveMode } from "./rpc-host.ts";
5
+
6
+ type AnswerContext = Pick<ExtensionContext, "sessionManager" | "cwd" | "mode" | "hasUI">;
7
+ export type HumanAnswer =
8
+ | { readonly kind: "option" | "custom"; readonly text: string; readonly selected?: readonly string[] }
9
+ | { readonly kind: "multi"; readonly selected: readonly string[] };
10
+ export interface HumanAnswerReceipt {
11
+ readonly sessionManager: ExtensionContext["sessionManager"];
12
+ readonly sessionId: string;
13
+ readonly cwd: string;
14
+ readonly branchAnchor: string | null;
15
+ readonly toolName: "ask_user_choice" | "ask_user_question";
16
+ readonly toolCallId: string;
17
+ readonly questionIndex: number;
18
+ /** Untrusted referent, never an instruction or an authorization. */
19
+ readonly question: string;
20
+ /** Full original prompt context, untrusted and inseparable from its answer. */
21
+ readonly prompt: string;
22
+ /** Authenticated human response; opaque tokens and previews are excluded. */
23
+ readonly answer: HumanAnswer;
24
+ }
25
+ interface Subscription {
26
+ ctx: AnswerContext;
27
+ sessionId: string;
28
+ cwd: string;
29
+ callback: (receipt: HumanAnswerReceipt) => void;
30
+ }
31
+ const subscriptions = new WeakMap<object, Set<Subscription>>();
32
+
33
+ /** Subscribe from a trusted host context; dispose on session shutdown/replacement. */
34
+ export function subscribeHumanAnswers(ctx: AnswerContext, callback: (receipt: HumanAnswerReceipt) => void): () => void {
35
+ if (!ctx.sessionManager || !isInteractiveMode(ctx.mode, process.env) || ctx.hasUI === false) return () => {};
36
+ const sessionId = ctx.sessionManager.getSessionId();
37
+ if (!sessionId || !ctx.cwd) return () => {};
38
+ let listeners = subscriptions.get(ctx.sessionManager);
39
+ if (!listeners) subscriptions.set(ctx.sessionManager, listeners = new Set());
40
+ const subscription = { ctx, sessionId, cwd: ctx.cwd, callback };
41
+ listeners.add(subscription);
42
+ return () => { listeners.delete(subscription); };
43
+ }
44
+
45
+ /** Host-only capture seam. Call before mounting UI, never from tool_result. */
46
+ function beginCapture(ctx: AnswerContext, toolName: HumanAnswerReceipt["toolName"], toolCallId: string, questions: ReadonlyArray<{ question: string; prompt: string }>, signal?: AbortSignal) {
47
+ const manager = ctx.sessionManager;
48
+ const sessionId = manager?.getSessionId();
49
+ const cwd = ctx.cwd;
50
+ const branchAnchor = manager?.getLeafId?.();
51
+ const listeners = manager ? subscriptions.get(manager) : undefined;
52
+ const eligible = [...(listeners ?? [])].filter((entry) => entry.sessionId === sessionId && entry.cwd === cwd);
53
+ const interactive = isInteractiveMode(ctx.mode, process.env) && ctx.hasUI !== false;
54
+ let finished = false;
55
+ return (answers: ReadonlyArray<{ questionIndex: number; answer: HumanAnswer }>) => {
56
+ if (finished) return;
57
+ finished = true;
58
+ if (!interactive || !isInteractiveMode(ctx.mode, process.env) || ctx.hasUI === false ||
59
+ !manager || !sessionId || signal?.aborted || ctx.sessionManager !== manager ||
60
+ manager.getSessionId() !== sessionId || ctx.cwd !== cwd || branchAnchor === undefined || manager.getLeafId() !== branchAnchor) return;
61
+ for (const { questionIndex, answer } of answers) {
62
+ const referent = questions[questionIndex];
63
+ if (referent === undefined) continue;
64
+ const { question, prompt } = referent;
65
+ const receipt = Object.freeze({ sessionManager: manager, sessionId, cwd, branchAnchor, toolName, toolCallId, questionIndex, question, prompt,
66
+ answer: Object.freeze({ ...answer, ...(answer.selected ? { selected: Object.freeze([...answer.selected]) } : {}) }) });
67
+ for (const entry of eligible) {
68
+ if (!listeners?.has(entry) || !isInteractiveMode(entry.ctx.mode, process.env) || entry.ctx.hasUI === false ||
69
+ entry.ctx.sessionManager !== manager || entry.ctx.cwd !== cwd ||
70
+ manager.getSessionId() !== sessionId || manager.getLeafId() !== branchAnchor || signal?.aborted) continue;
71
+ // Observers must not change the existing public tool outcome.
72
+ try { entry.callback(receipt); } catch { /* Isolate host observer failures. */ }
73
+ }
74
+ }
75
+ };
76
+ }
77
+
78
+ interface ChoicePrompt {
79
+ question: string;
80
+ options: Array<{ label: string; description?: string; value: string }>;
81
+ allowCustomResponse?: boolean;
82
+ }
83
+ export function captureChoiceAnswer(ctx: AnswerContext, toolCallId: string, params: ChoicePrompt, signal?: AbortSignal) {
84
+ const options = params.options.map((option) => ({ ...option }));
85
+ const allowCustom = params.allowCustomResponse === true;
86
+ const prompt = JSON.stringify({ question: params.question,
87
+ options: options.map(({ label, description }) => ({ label, description })), allowCustomResponse: allowCustom });
88
+ const publish = beginCapture(ctx, "ask_user_choice", toolCallId, [{ question: params.question, prompt }], signal);
89
+ return (selection: { value: string; label: string; index: number } | { customResponse: string } | undefined) => {
90
+ if (!selection) return;
91
+ if ("customResponse" in selection) {
92
+ if (allowCustom && typeof selection.customResponse === "string" && selection.customResponse.trim())
93
+ publish([{ questionIndex: 0, answer: { kind: "custom", text: selection.customResponse } }]);
94
+ return;
95
+ }
96
+ const option = options[selection.index - 1];
97
+ if (Number.isInteger(selection.index) && option && selection.label === option.label && selection.value === option.value)
98
+ publish([{ questionIndex: 0, answer: { kind: "option", text: option.label } }]);
99
+ };
100
+ }
101
+
102
+ export function captureQuestionnaireAnswers(ctx: AnswerContext, toolCallId: string, params: QuestionParams, signal?: AbortSignal) {
103
+ const questions = params.questions.map((question) => ({ ...question, options: question.options.map((option) => ({ ...option })) }));
104
+ const referents = questions.map((question) => ({ question: question.question, prompt: JSON.stringify({
105
+ question: question.question, header: question.header, multiSelect: question.multiSelect === true,
106
+ options: question.options.map(({ label, description, preview }) => ({ label, description,
107
+ ...(ctx.mode === "tui" && preview !== undefined ? { preview } : {}) })),
108
+ }) }));
109
+ const publish = beginCapture(ctx, "ask_user_question", toolCallId, referents, signal);
110
+ return (rows: AnswerRow[] | undefined) => {
111
+ if (!rows?.length) return;
112
+ const seen = new Set<number>();
113
+ const answers: Array<{ questionIndex: number; answer: HumanAnswer }> = [];
114
+ for (const row of rows) {
115
+ const question = questions[row.questionIndex];
116
+ if (!Number.isInteger(row.questionIndex) || !question || seen.has(row.questionIndex) || row.question !== question.question) return;
117
+ seen.add(row.questionIndex);
118
+ const labels = question.options.map((option) => option.label);
119
+ const selected = row.selected;
120
+ if (selected && (!question.multiSelect || new Set(selected).size !== selected.length || selected.some((label) => !labels.includes(label)))) return;
121
+ let answer: HumanAnswer;
122
+ if (row.kind === "option" && typeof row.answer === "string" && labels.includes(row.answer) && !question.multiSelect && !selected)
123
+ answer = { kind: "option", text: row.answer };
124
+ else if (row.kind === "custom" && typeof row.answer === "string" && row.answer.trim())
125
+ answer = { kind: "custom", text: row.answer, ...(selected ? { selected } : {}) };
126
+ else if (row.kind === "multi" && question.multiSelect && selected && row.answer === null)
127
+ answer = { kind: "multi", selected };
128
+ else return;
129
+ answers.push({ questionIndex: row.questionIndex, answer });
130
+ }
131
+ publish(answers);
132
+ };
133
+ }