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
@@ -286,14 +286,15 @@ function posixPrivateModeProbe(path: string): boolean {
286
286
  }
287
287
 
288
288
  function directory(path: string, privateMode = false, platform: NodeJS.Platform = process.platform, classifyParentPrivacy = false): string {
289
- const stat = lstatSync(path);
289
+ // 64-bit dev/ino may collide as Numbers; validate metadata and identity from one exact observation.
290
+ const stat = lstatSync(path, { bigint: true });
290
291
  if (!stat.isDirectory() || stat.isSymbolicLink() || !samePath(realpathSync(path), path, platform)) throw new Error("Unsafe candidate owner directory");
291
292
  if (privateMode && platform !== "win32") {
292
293
  const uid = process.getuid?.();
293
294
  if (uid === undefined) throw new Error("Unsafe candidate owner directory");
294
- if (stat.uid !== uid || (stat.mode & 0o077) !== 0) {
295
+ if (stat.uid !== BigInt(uid) || (stat.mode & 0o077n) !== 0n) {
295
296
  if (classifyParentPrivacy) {
296
- if (stat.uid === uid && (stat.mode & 0o077) !== 0 && !posixPrivateModeProbe(path)) throw new PosixCandidateOwnerParentChmodIneffectiveError();
297
+ if (stat.uid === BigInt(uid) && (stat.mode & 0o077n) !== 0n && !posixPrivateModeProbe(path)) throw new PosixCandidateOwnerParentChmodIneffectiveError();
297
298
  throw new PosixCandidateOwnerParentPrivacyError();
298
299
  }
299
300
  throw new Error("Unsafe candidate owner directory");
@@ -329,10 +330,11 @@ export function prepareCandidateOwnerParent(commonDir: string, platform: NodeJS.
329
330
  }
330
331
 
331
332
  function regular(path: string, privateMode = false, platform: NodeJS.Platform = process.platform): string {
332
- const stat = lstatSync(path);
333
+ // Keep 64-bit identity exact without a second stat lookup racing the privacy checks.
334
+ const stat = lstatSync(path, { bigint: true });
333
335
  const uid = process.getuid?.();
334
- if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || !samePath(realpathSync(path), path, platform) || stat.size > 16384 ||
335
- (privateMode && platform !== "win32" && (uid === undefined || stat.uid !== uid || (stat.mode & 0o777) !== 0o600))) throw new Error("Unsafe candidate owner file");
336
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n || !samePath(realpathSync(path), path, platform) || stat.size > 16384n ||
337
+ (privateMode && platform !== "win32" && (uid === undefined || stat.uid !== BigInt(uid) || (stat.mode & 0o777n) !== 0o600n))) throw new Error("Unsafe candidate owner file");
336
338
  if (privateMode && platform === "win32") privateWindowsDacl(path, "file", false);
337
339
  return `${stat.dev}:${stat.ino}`;
338
340
  }
@@ -1861,7 +1861,7 @@ export function decodeCandidateContextManifest(encoded: string, sha256: string):
1861
1861
  if (actualSha256 !== sha256) throw new CandidateViewError("candidate context manifest integrity check failed", "candidate-context-manifest-integrity");
1862
1862
  const text = bytes.toString("utf8");
1863
1863
  if (!Buffer.from(text, "utf8").equals(bytes)) return invalidCandidateContextManifest("candidate context manifest is not valid UTF-8");
1864
- if (gzipSync(bytes, { mtime: 0 }).toString("base64url") !== encoded) {
1864
+ if (gzipSync(bytes).toString("base64url") !== encoded) {
1865
1865
  return invalidCandidateContextManifest("candidate context manifest transport is not canonical");
1866
1866
  }
1867
1867
  let value: unknown;
@@ -1923,7 +1923,8 @@ function compactCandidateContextBlock(lineageId: string, agents: readonly Review
1923
1923
  const bytes = Buffer.from(JSON.stringify(manifest), "utf8");
1924
1924
  if (bytes.length > MAX_CANDIDATE_CONTEXT_MANIFEST_BYTES) throw new CandidateViewError("candidate view context exceeds the bounded dispatch contract");
1925
1925
  const sha256 = createHash("sha256").update(bytes).digest("hex");
1926
- const encoded = gzipSync(bytes, { mtime: 0 }).toString("base64url");
1926
+ // Node emits a zero gzip timestamp by default; no unsupported mtime option is needed.
1927
+ const encoded = gzipSync(bytes).toString("base64url");
1927
1928
  const block = `${candidateContextPreamble(lineageId, agents, view, scopeSemantics)}\nFrozen changed scope manifest (gzip+base64url): \`${encoded}\`.\nFrozen changed scope manifest SHA-256: \`${sha256}\`.\nCall \`gentle_review_scope\` with exactly this manifest, SHA-256, and cursor 0; continue with each returned \`nextCursor\` until absent. It is the only authorized scope enumerator: do not infer scope by traversing the candidate or ambient tree. Gitlinks are metadata-only and MUST NOT be traversed.\nThe ambient contributor working directory is out of scope. This controller-owned context is immutable; you are read-only and your output is untrusted.`;
1928
1929
  if (Buffer.byteLength(block, "utf8") > MAX_CANDIDATE_CONTEXT_LENGTH) throw new CandidateViewError("candidate view context exceeds the bounded dispatch contract");
1929
1930
  return block;
@@ -136,7 +136,7 @@ function parseCompactFinalizeInputValue(value: unknown): CompactFinalizeContract
136
136
  if ((input.final_evidence === undefined && outcomeFields !== 0) || (input.final_evidence !== undefined && outcomeFields !== 1)) fail("review/finalize", "field-pair", "final evidence requires exactly one verification result or outcome");
137
137
  let correction_line_forecast: number | undefined;
138
138
  if (input.correction_line_forecast !== undefined) {
139
- if (!Number.isSafeInteger(input.correction_line_forecast) || input.correction_line_forecast <= 0) fail("review/finalize.correction_line_forecast", "range", "must be a positive safe integer");
139
+ if (typeof input.correction_line_forecast !== "number" || !Number.isSafeInteger(input.correction_line_forecast) || input.correction_line_forecast <= 0) fail("review/finalize.correction_line_forecast", "range", "must be a positive safe integer");
140
140
  correction_line_forecast = input.correction_line_forecast;
141
141
  }
142
142
  if (input.final_verification_passed !== undefined && typeof input.final_verification_passed !== "boolean") fail("review/finalize.final_verification_passed", "type", "must be boolean");
@@ -154,7 +154,7 @@ function parseCompactFinalizeInputValue(value: unknown): CompactFinalizeContract
154
154
  if (typeof input.final_evidence !== "string" || input.final_evidence.length === 0) fail("review/finalize.final_evidence", "empty", "must contain at least one byte");
155
155
  final_evidence = input.final_evidence;
156
156
  }
157
- return { cwd: string(input.cwd, "review/finalize.cwd"), ...(optionalLineage(input.lineageId, "review/finalize.lineageId") === undefined ? {} : { lineageId: optionalLineage(input.lineageId, "review/finalize.lineageId")! }), ...(correction_line_forecast === undefined ? {} : { correction_line_forecast }), ...(input.validation === undefined ? {} : { validation: parseValidation(input.validation, "review/finalize.validation") }), ...(final_evidence === undefined ? {} : { final_evidence }), ...(input.final_verification_passed === undefined ? {} : { final_verification_passed: input.final_verification_passed }), ...(final_verification_outcome === undefined ? {} : { final_verification_outcome }), ...(input.reviewer_run_acknowledged === undefined ? {} : { reviewer_run_acknowledged: input.reviewer_run_acknowledged as boolean }) };
157
+ return { cwd: string(input.cwd, "review/finalize.cwd"), ...(optionalLineage(input.lineageId, "review/finalize.lineageId") === undefined ? {} : { lineageId: optionalLineage(input.lineageId, "review/finalize.lineageId")! }), ...(correction_line_forecast === undefined ? {} : { correction_line_forecast }), ...(input.validation === undefined ? {} : { validation: parseValidation(input.validation, "review/finalize.validation") }), ...(final_evidence === undefined ? {} : { final_evidence }), ...(typeof input.final_verification_passed !== "boolean" ? {} : { final_verification_passed: input.final_verification_passed }), ...(final_verification_outcome === undefined ? {} : { final_verification_outcome }), ...(typeof input.reviewer_run_acknowledged !== "boolean" ? {} : { reviewer_run_acknowledged: input.reviewer_run_acknowledged }) };
158
158
  }
159
159
 
160
160
  export function parseNativeCompactFinalizeInput(value: unknown): CompactFinalizeContractInput {
@@ -34,13 +34,14 @@ import { spawn } from "node:child_process";
34
34
  import { tmpdir } from "node:os";
35
35
  import { isAbsolute, join } from "node:path";
36
36
  import { completeSimple } from "@earendil-works/pi-ai/compat";
37
- import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
37
+ import { gentleAiIsolationEnv, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
38
38
  import {
39
39
  INPROCESS_REVIEWER_FAILURE,
40
40
  runInProcessReviewer,
41
41
  type InProcessReviewerFailureCode,
42
42
  type InProcessReviewerOutcome,
43
43
  type InProcessReviewerRegistry,
44
+ type InProcessReviewerRequest,
44
45
  } from "./inprocess-reviewer.ts";
45
46
  import { REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION, REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3 } from "./review-integration-v2.ts";
46
47
  import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
@@ -386,6 +387,8 @@ export interface ReviewHostRelayRequest {
386
387
  readonly selection?: string;
387
388
  /** The routing entry's thinking label, forwarded verbatim to the completion. */
388
389
  readonly thinking?: string;
390
+ /** Effective Pi transport preference, forwarded without forcing a provider default. */
391
+ readonly reviewerTransport?: InProcessReviewerRequest["transport"];
389
392
  /**
390
393
  * The caller's live pi session id, forwarded into the in-process completion
391
394
  * request so an OpenCode-routed reviewer model carries its
@@ -498,7 +501,8 @@ function collectGentleAiProcess(
498
501
  const startedAt = Date.now();
499
502
  const child = spawn(file, [...arguments_], {
500
503
  cwd: options.cwd,
501
- env: options.env,
504
+ // S9: a bundled install keeps Gentle AI's state inside its prefix.
505
+ env: gentleAiIsolationEnv(options.env),
502
506
  stdio: ["pipe", "pipe", "pipe"],
503
507
  shell: false,
504
508
  windowsHide: true,
@@ -737,6 +741,7 @@ export async function prepareReviewHostRelaySlot(
737
741
  {
738
742
  selection: preparedRequest.selection!,
739
743
  ...(preparedRequest.thinking === undefined ? {} : { thinking: preparedRequest.thinking }),
744
+ ...(preparedRequest.reviewerTransport === undefined ? {} : { transport: preparedRequest.reviewerTransport }),
740
745
  ...(preparedRequest.reviewerSessionId === undefined ? {} : { sessionId: preparedRequest.reviewerSessionId }),
741
746
  prompt: promptBytes,
742
747
  timeoutMs: piTimeoutMs,
@@ -77,8 +77,8 @@ export type ReviewStartState = (typeof REVIEW_START_STATE)[keyof typeof REVIEW_S
77
77
  const START_ACTIONS = ["created", "resumed", "closed", "blocked-scope-action"] as const;
78
78
  const RISK_LEVELS = ["low", "medium", "high"] as const;
79
79
  const REVIEW_LENSES = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
80
- const RISK_REASON_CODES = ["configuration_change", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] as const;
81
- const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process"] as const;
80
+ const RISK_REASON_CODES = ["agent_escalation", "configuration_change", "dangerous_sink", "empty_content", "executable_change", "executable_mode", "hot_path", "large_change", "non_executable_only", "process_boundary", "process_scan_limit", "service_token", "shell_source"] as const;
81
+ const RISK_SIGNALS = ["auth", "update", "security", "payments", "permissions", "shell_process", "dangerous_sink", "agent_escalation"] as const;
82
82
  // "collect" and "execute" are the v2 envelope's projection of a live
83
83
  // transaction whose next_transition is mandatory: the root action names the
84
84
  // transition kind instead of reading as a terminal stop.
@@ -636,6 +636,23 @@ export interface ReviewEscalationV1 {
636
636
  refuterOutcomes?: readonly ReviewEscalationRefuterOutcomeV1[];
637
637
  }
638
638
 
639
+ function decodeReviewEscalationV1(value: unknown, label: string): ReviewEscalationV1 {
640
+ const source = exactRecord(value, label, ["cause", "finding_ids"], ["refuter_outcomes"]);
641
+ const escalation: ReviewEscalationV1 = {
642
+ cause: enumeration(source.cause, ESCALATION_CAUSES, `${label}.cause`),
643
+ findingIds: stringArray(source.finding_ids, `${label}.finding_ids`),
644
+ };
645
+ if (Object.hasOwn(source, "refuter_outcomes")) escalation.refuterOutcomes = array(source.refuter_outcomes, `${label}.refuter_outcomes`, (entry, itemLabel) => {
646
+ const row = exactRecord(entry, itemLabel, ["finding_id", "outcome", "proof"]);
647
+ return {
648
+ findingId: nonempty(row.finding_id, `${itemLabel}.finding_id`),
649
+ outcome: enumeration(row.outcome, ESCALATION_REFUTER_OUTCOMES, `${itemLabel}.outcome`),
650
+ proof: nonempty(row.proof, `${itemLabel}.proof`),
651
+ };
652
+ });
653
+ return escalation;
654
+ }
655
+
639
656
  export interface ReviewStatusV3 {
640
657
  /** Optional v7 diagnostic evidence, never routing or reconstructed authority. */
641
658
  escalation?: ReviewEscalationV1;
@@ -2104,23 +2121,9 @@ export function decodeReviewStatusV3(value: unknown): ReviewStatusV3 {
2104
2121
  };
2105
2122
  }
2106
2123
 
2107
- let escalation: ReviewEscalationV1 | undefined;
2108
- if (Object.hasOwn(body, "escalation")) {
2109
- const label = "status.escalation";
2110
- const source = exactRecord(body.escalation, label, ["cause", "finding_ids"], ["refuter_outcomes"]);
2111
- escalation = {
2112
- cause: enumeration(source.cause, ESCALATION_CAUSES, `${label}.cause`),
2113
- findingIds: stringArray(source.finding_ids, `${label}.finding_ids`),
2114
- };
2115
- if (Object.hasOwn(source, "refuter_outcomes")) escalation.refuterOutcomes = array(source.refuter_outcomes, `${label}.refuter_outcomes`, (entry, itemLabel) => {
2116
- const row = exactRecord(entry, itemLabel, ["finding_id", "outcome", "proof"]);
2117
- return {
2118
- findingId: nonempty(row.finding_id, `${itemLabel}.finding_id`),
2119
- outcome: enumeration(row.outcome, ESCALATION_REFUTER_OUTCOMES, `${itemLabel}.outcome`),
2120
- proof: nonempty(row.proof, `${itemLabel}.proof`),
2121
- };
2122
- });
2123
- }
2124
+ const escalation = Object.hasOwn(body, "escalation")
2125
+ ? decodeReviewEscalationV1(body.escalation, "status.escalation")
2126
+ : undefined;
2124
2127
 
2125
2128
  // status/v7 top-level optional digest (gentle-ai v2.6.0): resolves #4066's
2126
2129
  // closed loop where `sdd-attempt finish` named a digest status never
@@ -2583,7 +2586,11 @@ function assertReviewApprovedAcknowledgementExecuteShapeV1(execute: ReviewNextTr
2583
2586
  if (execute.arguments.length !== REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS.length) throw new TypeError(`acknowledgement.execute.arguments must carry exactly ${REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS.length} provider-issued arguments`);
2584
2587
  const values = execute.arguments.map((argument, index) => { const name = REVIEW_APPROVED_ACKNOWLEDGEMENT_ARGUMENTS[index]!; if (argument.name !== name) throw new TypeError(`acknowledgement.execute.arguments[${index}].name must be ${name}`); const value = nonempty(argument.value, `acknowledgement.execute.arguments[${index}].value`); if (nonempty(argument.token, `acknowledgement.execute.arguments[${index}].token`) !== `--${name}=${value}`) throw new TypeError(`acknowledgement.execute.arguments[${index}].token must exactly match ${name}`); return value; });
2585
2588
  if (execute.preconditions.length !== 1 || execute.preconditions[0]?.name !== "state" || execute.preconditions[0]?.value !== "approved") throw new TypeError("acknowledgement.execute.preconditions must be the single approved state precondition");
2586
- return { tokens: execute.arguments.map((argument) => argument.token!) as ReviewApprovedAcknowledgementExecuteTokensV1, values: values as readonly [string, string, string, string, string], lineageId: lineage(execute.binding.lineageId, "acknowledgement.execute.binding.lineage_id"), targetIdentity: sha256(execute.binding.targetIdentity, "acknowledgement.execute.binding.target_identity"), revision: sha256(execute.binding.revision, "acknowledgement.execute.binding.revision") };
2589
+ const tokens = execute.arguments.map((argument) => argument.token!);
2590
+ // The exact five-argument check above validates both vectors before tuple construction.
2591
+ const tokenTuple: ReviewApprovedAcknowledgementExecuteTokensV1 = [tokens[0]!, tokens[1]!, tokens[2]!, tokens[3]!, tokens[4]!];
2592
+ const valueTuple: readonly [string, string, string, string, string] = [values[0]!, values[1]!, values[2]!, values[3]!, values[4]!];
2593
+ return { tokens: tokenTuple, values: valueTuple, lineageId: lineage(execute.binding.lineageId, "acknowledgement.execute.binding.lineage_id"), targetIdentity: sha256(execute.binding.targetIdentity, "acknowledgement.execute.binding.target_identity"), revision: sha256(execute.binding.revision, "acknowledgement.execute.binding.revision") };
2587
2594
  }
2588
2595
 
2589
2596
  /**
@@ -2682,6 +2689,26 @@ export interface ReviewLastEventReviewerResultV1 {
2682
2689
  resultHash: string;
2683
2690
  }
2684
2691
 
2692
+ export interface ReviewTargetedValidatorCheckEvidenceV1 {
2693
+ passed: boolean;
2694
+ evidence: readonly string[];
2695
+ regressions?: readonly {
2696
+ id?: string;
2697
+ location: string;
2698
+ claim: string;
2699
+ proofRefs: readonly string[];
2700
+ }[];
2701
+ }
2702
+
2703
+ export interface ReviewTargetedValidatorEvidenceV1 {
2704
+ targetedValidationRequestHash: string;
2705
+ correctionTargetIdentity: string;
2706
+ originalCriteria: ReviewTargetedValidatorCheckEvidenceV1;
2707
+ correctionRegression: ReviewTargetedValidatorCheckEvidenceV1;
2708
+ followUps: readonly { observation: string; proofRefs: readonly string[] }[];
2709
+ raw: Record<string, unknown>;
2710
+ }
2711
+
2685
2712
  export interface ReviewLastEventClosureV1 {
2686
2713
  schema: typeof REVIEW_LAST_EVENT_CLOSURE_SCHEMA;
2687
2714
  operation: ReviewLastEventClosureOperation;
@@ -2692,6 +2719,8 @@ export interface ReviewLastEventClosureV1 {
2692
2719
  targetIdentity?: string;
2693
2720
  requestHash?: string;
2694
2721
  correctionLines?: number;
2722
+ escalation?: ReviewEscalationV1;
2723
+ targetedValidatorEvidence?: ReviewTargetedValidatorEvidenceV1;
2695
2724
  advisoryFindings?: ReviewAdvisoryFindingsV1;
2696
2725
  reviewerResults?: readonly ReviewLastEventReviewerResultV1[];
2697
2726
  statusContinuation?: ReviewStatusContinuationV1;
@@ -2835,18 +2864,67 @@ function decodeReviewLastEventReviewerResultV1(value: unknown, label: string): R
2835
2864
  };
2836
2865
  }
2837
2866
 
2867
+ function decodeReviewTargetedValidatorCheckEvidenceV1(value: unknown, label: string): ReviewTargetedValidatorCheckEvidenceV1 {
2868
+ const check = exactRecord(value, label, ["passed", "evidence"], ["regressions"]);
2869
+ const regressions = Object.hasOwn(check, "regressions")
2870
+ ? array(check.regressions, `${label}.regressions`, (entry, entryLabel) => {
2871
+ const row = exactRecord(entry, entryLabel, ["location", "claim", "proof_refs"], ["id"]);
2872
+ return {
2873
+ ...(Object.hasOwn(row, "id") ? { id: nonempty(row.id, `${entryLabel}.id`) } : {}),
2874
+ location: nonempty(row.location, `${entryLabel}.location`),
2875
+ claim: nonempty(row.claim, `${entryLabel}.claim`),
2876
+ proofRefs: stringArray(row.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }),
2877
+ };
2878
+ }) : undefined;
2879
+ return {
2880
+ passed: boolean(check.passed, `${label}.passed`),
2881
+ // Native admission rejects observations containing only Unicode whitespace.
2882
+ evidence: stringArray(check.evidence, `${label}.evidence`, { minimum: 1, pattern: /[^\p{White_Space}]/u }),
2883
+ ...(regressions === undefined ? {} : { regressions }),
2884
+ };
2885
+ }
2886
+
2887
+ function decodeReviewTargetedValidatorEvidenceV1(value: unknown, label: string): ReviewTargetedValidatorEvidenceV1 {
2888
+ const body = exactRecord(value, label, ["targeted_validation_request_hash", "correction_target_identity", "original_criteria", "correction_regression", "follow_ups"]);
2889
+ return {
2890
+ targetedValidationRequestHash: sha256(body.targeted_validation_request_hash, `${label}.targeted_validation_request_hash`),
2891
+ correctionTargetIdentity: sha256(body.correction_target_identity, `${label}.correction_target_identity`),
2892
+ originalCriteria: decodeReviewTargetedValidatorCheckEvidenceV1(body.original_criteria, `${label}.original_criteria`),
2893
+ correctionRegression: decodeReviewTargetedValidatorCheckEvidenceV1(body.correction_regression, `${label}.correction_regression`),
2894
+ followUps: array(body.follow_ups, `${label}.follow_ups`, (entry, entryLabel) => {
2895
+ const row = exactRecord(entry, entryLabel, ["observation", "proof_refs"]);
2896
+ return { observation: nonempty(row.observation, `${entryLabel}.observation`), proofRefs: stringArray(row.proof_refs, `${entryLabel}.proof_refs`, { minimum: 1 }) };
2897
+ }),
2898
+ raw: body,
2899
+ };
2900
+ }
2901
+
2838
2902
  export function decodeReviewLastEventClosureV1(value: unknown): ReviewLastEventClosureV1 {
2839
- const body = exactRecord(value, "last_event_closure", ["schema", "operation", "lineage_id", "state", "store_revision"], ["target_identity", "request_hash", "correction_lines", "action", "advisory_findings", "reviewer_results", "status_continuation", "acknowledgement"]);
2903
+ const body = exactRecord(value, "last_event_closure", ["schema", "operation", "lineage_id", "state", "store_revision"], ["target_identity", "request_hash", "correction_lines", "action", "escalation", "targeted_validator_evidence", "advisory_findings", "reviewer_results", "status_continuation", "acknowledgement"]);
2840
2904
  if (body.schema !== REVIEW_LAST_EVENT_CLOSURE_SCHEMA) throw new TypeError(`last_event_closure.schema must be ${REVIEW_LAST_EVENT_CLOSURE_SCHEMA}`);
2841
2905
  const operation = enumeration(body.operation, Object.values(REVIEW_LAST_EVENT_CLOSURE_OPERATION), "last_event_closure.operation") as ReviewLastEventClosureOperation;
2842
2906
  const state = enumeration(body.state, REVIEW_LAST_EVENT_TERMINAL_STATES, "last_event_closure.state") as ReviewLastEventClosureState;
2907
+ if (Object.hasOwn(body, "targeted_validator_evidence") && (operation !== REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_VALIDATION || state !== "escalated")) {
2908
+ throw new TypeError("last_event_closure targeted_validator_evidence requires escalated review/capture-validation");
2909
+ }
2910
+ // Diagnostic evidence is optional for older providers and never controls terminal authority.
2911
+ const targetedValidatorEvidence = Object.hasOwn(body, "targeted_validator_evidence")
2912
+ ? decodeReviewTargetedValidatorEvidenceV1(body.targeted_validator_evidence, "last_event_closure.targeted_validator_evidence")
2913
+ : undefined;
2914
+ if (Object.hasOwn(body, "escalation") && state !== "escalated") throw new TypeError("last_event_closure escalation requires escalated state");
2915
+ // Older native closures omit this diagnostic; its presence never grants a continuation.
2916
+ const escalation = Object.hasOwn(body, "escalation")
2917
+ ? decodeReviewEscalationV1(body.escalation, "last_event_closure.escalation")
2918
+ : undefined;
2843
2919
  const shared = {
2844
2920
  schema: REVIEW_LAST_EVENT_CLOSURE_SCHEMA,
2845
2921
  operation,
2846
2922
  lineageId: lineage(body.lineage_id, "last_event_closure.lineage_id"),
2847
2923
  state,
2848
2924
  storeRevision: sha256(body.store_revision, "last_event_closure.store_revision"),
2849
- };
2925
+ ...(escalation === undefined ? {} : { escalation }),
2926
+ ...(targetedValidatorEvidence === undefined ? {} : { targetedValidatorEvidence }),
2927
+ } as const;
2850
2928
  if (operation === REVIEW_LAST_EVENT_CLOSURE_OPERATION.CAPTURE_CORRECTION_PLAN) {
2851
2929
  if (body.reviewer_results !== undefined) throw new TypeError("last_event_closure reviewer_results requires approved state");
2852
2930
  if (body.action !== undefined || body.advisory_findings !== undefined || body.status_continuation !== undefined) throw new TypeError("last_event_closure correction-plan cannot carry action, advisory_findings, or status_continuation");
@@ -393,7 +393,7 @@ function deriveReleaseCiStatusForShaV1(options: {
393
393
  } catch {
394
394
  return { proven: false, status: null };
395
395
  }
396
- if (!isRecord(summary) || !Number.isSafeInteger(summary.total_count) || !Number.isSafeInteger(summary.returned) || !Array.isArray(summary.checks) || summary.total_count < 0 || summary.returned < 0 || summary.returned !== summary.checks.length || summary.total_count !== summary.checks.length) {
396
+ if (!isRecord(summary) || typeof summary.total_count !== "number" || typeof summary.returned !== "number" || !Number.isSafeInteger(summary.total_count) || !Number.isSafeInteger(summary.returned) || !Array.isArray(summary.checks) || summary.total_count < 0 || summary.returned < 0 || summary.returned !== summary.checks.length || summary.total_count !== summary.checks.length) {
397
397
  return { proven: false, status: null };
398
398
  }
399
399
  if (summary.total_count > 0) {
@@ -139,18 +139,23 @@ const DUE_REVIEW_REASONS: readonly ReviewDueReason[] = [REVIEW_DUE_REASON.HIGH_R
139
139
 
140
140
  /**
141
141
  * Rejects a review_due/review_due_reason/consumed combination the native
142
- * schema can never produce. `already_reviewed` takes precedence exactly when
143
- * the candidate is consumed, so the two must agree in both directions. A
144
- * contradictory envelope is never trusted, least of all as closure evidence.
145
- * Envelopes without the pair (older binaries) are not checked here.
142
+ * schema can never produce. A consumed candidate must report
143
+ * `already_reviewed`, but that reason can also describe an acknowledged
144
+ * committed predecessor followed by a passive delta (#1954). It does not
145
+ * prove this exact candidate was consumed; preserve native's explicit false.
146
+ * Native v2 requires consumed, so missing evidence with already_reviewed is
147
+ * still rejected. Envelopes without the pair (older binaries) are unchecked.
146
148
  */
147
149
  function validateReviewDueConsistency(reviewDue: boolean, reviewDueReason: ReviewDueReason, candidate: ReviewAssessmentCandidate): void {
148
150
  if (reviewDue !== DUE_REVIEW_REASONS.includes(reviewDueReason)) {
149
151
  throw new TypeError(`review assessment review_due ${reviewDue} contradicts review_due_reason ${reviewDueReason}`);
150
152
  }
151
153
  const alreadyReviewed = reviewDueReason === REVIEW_DUE_REASON.ALREADY_REVIEWED;
152
- if (alreadyReviewed !== (candidate.consumed === true)) {
153
- throw new TypeError("review assessment review_due_reason already_reviewed must be reported exactly when candidate.consumed is true");
154
+ if (candidate.consumed === true && !alreadyReviewed) {
155
+ throw new TypeError("review assessment candidate.consumed true requires review_due_reason already_reviewed");
156
+ }
157
+ if (alreadyReviewed && candidate.consumed === undefined) {
158
+ throw new TypeError("review assessment review_due_reason already_reviewed requires explicit candidate.consumed evidence");
154
159
  }
155
160
  }
156
161
 
@@ -259,6 +264,53 @@ export const VERIFICATION_TIER = {
259
264
  } as const;
260
265
  export type VerificationTier = (typeof VERIFICATION_TIER)[keyof typeof VERIFICATION_TIER];
261
266
 
267
+ // ---------------------------------------------------------------------------
268
+ // Agent-raised risk (gentle-shell#1494). The agent that made the change may
269
+ // raise its risk to high by citing one item of the high-risk list in the
270
+ // always-on Task Size section; it can never lower a tier. Lowering stays with
271
+ // the deterministic native assessment.
272
+ // ---------------------------------------------------------------------------
273
+
274
+ export const HIGH_RISK_ITEMS = Object.freeze({
275
+ 1: "data or irreversible effects",
276
+ 2: "security",
277
+ 3: "changing or removing contracts others already consume",
278
+ 4: "concurrency",
279
+ 5: "delivery or environment",
280
+ 6: "no test would catch a regression",
281
+ } as const);
282
+ export type HighRiskItem = keyof typeof HIGH_RISK_ITEMS;
283
+
284
+ export interface AgentRiskEscalation {
285
+ readonly item: HighRiskItem;
286
+ readonly reason: string;
287
+ }
288
+
289
+ export const AGENT_RISK_ESCALATION_REASON_MAX = 500;
290
+
291
+ export function decodeAgentRiskEscalation(value: unknown): AgentRiskEscalation {
292
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
293
+ throw new Error("escalate must be an object with item and reason");
294
+ }
295
+ const record = value as Record<string, unknown>;
296
+ const unexpected = Object.keys(record).find((key) => key !== "item" && key !== "reason");
297
+ if (unexpected !== undefined) throw new Error(`escalate does not accept ${unexpected}`);
298
+ const { item, reason } = record;
299
+ if (typeof item !== "number" || !Number.isInteger(item) || !(item in HIGH_RISK_ITEMS)) {
300
+ throw new Error("escalate item must be an integer from 1 to 6 naming a high-risk item");
301
+ }
302
+ if (typeof reason !== "string" || reason.trim() === "" || reason.length > AGENT_RISK_ESCALATION_REASON_MAX) {
303
+ throw new Error(`escalate reason must be a non-empty string of at most ${AGENT_RISK_ESCALATION_REASON_MAX} characters`);
304
+ }
305
+ return Object.freeze({ item: item as HighRiskItem, reason });
306
+ }
307
+
308
+ /** Raises passive or medium to high when the agent escalated; never lowers. */
309
+ export function escalatedRisk(risk: VerificationTier, escalation: AgentRiskEscalation | undefined): VerificationTier {
310
+ if (escalation === undefined) return risk;
311
+ return risk === VERIFICATION_TIER.PASSIVE || risk === VERIFICATION_TIER.MEDIUM ? VERIFICATION_TIER.HIGH : risk;
312
+ }
313
+
262
314
  export const RDD_LINE = {
263
315
  ON: "on",
264
316
  OFF: "off",
@@ -363,59 +415,33 @@ function nonClosedOutcomeClause(outcome: NativeReviewOutcome): string {
363
415
  }
364
416
 
365
417
  /**
366
- * Computes who verifies a delegated writer's change, exactly as
367
- * gentle-pi#662/#668 specify:
418
+ * Computes who verifies a delegated writer's change. Verification follows the
419
+ * native risk tier whatever the RDD line and native review outcome
420
+ * (verify-always-rdd-high S1, superseding the gentle-pi#662/#668 on-path): the
421
+ * native review is an additional outside view and never replaces or skips the
422
+ * tier's verification. `nativeReviewOutcome` only shapes the reason text.
368
423
  *
369
- * - `rdd: "on"` AND the native review reached `NATIVE_REVIEW_OUTCOME.CLOSED`
370
- * for this candidate: the writer self-verifies and no independent verifier
371
- * runs, because the closed native review is the check -- except `passive`,
372
- * which gets a structural readback by the parent instead.
373
- * - `rdd: "on"` with any other outcome (`declined`, `unavailable`, or the
374
- * fail-closed `unknown` default for an omitted/unrecognized outcome): the
375
- * `on` branch never held for this candidate, so the risk-gated path below
376
- * applies exactly as `off` -- declining a review is candidate-scoped and
377
- * never lowers the bar below the RDD-off path.
378
- * - `rdd: "off"` or `"unknown"` (both gate identically, so an unknown RDD line
379
- * never lowers a tier relative to `off`; `nativeReviewOutcome` is ignored
380
- * entirely on these lines):
381
- * - `passive`: structural readback by the parent only; no verifier, no
382
- * tests.
383
- * - `medium`: the writer self-verifies; a separate independent verifier
384
- * runs only when `writerProfile` is `"small"` -- the small-model bias
385
- * raises the tier by one for verification purposes (medium -> high). An
386
- * unknown or omitted writer profile resolves to `"small"` before it
387
- * reaches this function (see `resolveWriterProfile`), so an unrecognized
388
- * profile never lowers a tier below what a known small model would get.
389
- * - `high` or `unassessable` (a failed or unrecognized native assessment is
390
- * always treated as high): the writer self-verifies and an independent
391
- * verifier always runs.
424
+ * - `passive`: structural readback by the parent only; no verifier, no tests.
425
+ * - `medium`: the writer self-verifies; a separate independent verifier runs
426
+ * only when `writerProfile` is `"small"` -- the small-model bias raises the
427
+ * tier by one for verification purposes (medium -> high). An unknown or
428
+ * omitted writer profile resolves to `"small"` before it reaches this
429
+ * function (see `resolveWriterProfile`), so an unrecognized profile never
430
+ * lowers a tier below what a known small model would get.
431
+ * - `high` or `unassessable` (a failed or unrecognized native assessment is
432
+ * always treated as high): the writer self-verifies and an independent
433
+ * verifier always runs.
392
434
  */
393
435
  export function verificationPlan(input: VerificationPlanInput): VerificationPlan {
394
436
  const { rddLine, risk, writerProfile } = input;
395
437
 
396
438
  if (rddLine === RDD_LINE.ON) {
397
439
  const outcome = input.nativeReviewOutcome ?? NATIVE_REVIEW_OUTCOME.UNKNOWN;
398
- if (outcome === NATIVE_REVIEW_OUTCOME.CLOSED) {
399
- if (risk === VERIFICATION_TIER.PASSIVE) {
400
- return Object.freeze({
401
- writerSelfVerification: false,
402
- structuralReadbackOnly: true,
403
- independentVerifier: false,
404
- reason: "receipt-driven development is on and the native review closed for this candidate; the assessment also reports passive risk: a structural readback by the parent replaces both the writer's self-verification and any separate verifier.",
405
- });
406
- }
407
- return Object.freeze({
408
- writerSelfVerification: true,
409
- structuralReadbackOnly: false,
410
- independentVerifier: false,
411
- reason: "receipt-driven development is on and the native review closed for this candidate: the writer's self-verification is the record and the closed native review was the independent check the writer cannot influence, so no separate verifier is required.",
412
- });
413
- }
414
- // The on branch holds only while the native review reaches a terminal
415
- // outcome for this candidate. Anything else -- declined, unavailable, or
416
- // unknown -- falls back to the exact same risk-gated path as off.
440
+ const clause = outcome === NATIVE_REVIEW_OUTCOME.CLOSED
441
+ ? "the native review closed for this candidate"
442
+ : nonClosedOutcomeClause(outcome);
417
443
  return riskGatedPlan(
418
- `receipt-driven development is on, but ${nonClosedOutcomeClause(outcome)}, so the risk-gated path applies exactly as receipt-driven development off, and`,
444
+ `receipt-driven development is on and ${clause}; the native review is an additional outside view that never replaces verification, so the risk-gated path applies exactly as receipt-driven development off, and`,
419
445
  risk,
420
446
  writerProfile,
421
447
  );
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-w
2
2
  import type { TSchema } from "typebox";
3
3
  import { NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError } from "./native-review-cli.ts";
4
4
  import { sanitizeTerminalText } from "./terminal-theme.ts";
5
+ import { measureStatusSidebar } from "./status-timing-diagnostics.ts";
5
6
 
6
7
  /** Ephemeral display data only; this event grants no review authority. */
7
8
  export const REVIEW_SIDEBAR_EVENT = "gentle-ai:review-sidebar";
@@ -163,7 +164,7 @@ export function createReviewSidebarPublisher(pi: ExtensionAPI) {
163
164
  let generation = 0;
164
165
  let scope: ReviewScope | undefined;
165
166
  const publish = (id: string, snapshot: ReviewSidebarSnapshot) => {
166
- try { pi.events?.emit(REVIEW_SIDEBAR_EVENT, { sessionId: id, snapshot }); } catch { /* Display only. */ }
167
+ try { measureStatusSidebar(() => pi.events?.emit(REVIEW_SIDEBAR_EVENT, { sessionId: id, snapshot })); } catch { /* Display only. */ }
167
168
  };
168
169
  return {
169
170
  reset(ctx?: ExtensionContext) {
@@ -994,11 +994,80 @@ function operationForTransition(transition: ReviewTransition): ReviewOperation {
994
994
  throw new ReviewIntegrityError(`Unsupported reducer transition: ${transition}`);
995
995
  }
996
996
 
997
+ function assertReducerInput(transition: ReviewTransition, input: unknown): asserts input is ReviewReducerInput {
998
+ const fail = (field: string): never => { throw new ReviewIntegrityError(`Graph reducer input ${field} is invalid`); };
999
+ const object = (value: unknown, field: string): Record<string, unknown> => {
1000
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return fail(field);
1001
+ return value as Record<string, unknown>;
1002
+ };
1003
+ const string = (value: unknown, field: string): void => { if (typeof value !== "string") fail(field); };
1004
+ const boolean = (value: unknown, field: string): void => { if (typeof value !== "boolean") fail(field); };
1005
+ const strings = (value: unknown, field: string): void => { if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) fail(field); };
1006
+ const rows = (value: unknown, field: string): void => {
1007
+ if (!Array.isArray(value)) return fail(field);
1008
+ // The canonical row decoder checks every field, enum and duplicate ID.
1009
+ createFrozenLedger(value);
1010
+ };
1011
+ const results = (value: unknown, field: string): void => {
1012
+ if (!Array.isArray(value)) return fail(field);
1013
+ for (const item of value) {
1014
+ const result = object(item, field);
1015
+ string(result.id, `${field}.id`);
1016
+ if (!Object.values(RESOLUTION_OUTCOME).some((outcome) => outcome === result.outcome)) fail(`${field}.outcome`);
1017
+ }
1018
+ };
1019
+ const body = object(input, "object");
1020
+ switch (transition) {
1021
+ case REVIEW_TRANSITION.ORDINARY_DISCOVERY: rows(body.rows, "rows"); break;
1022
+ case REVIEW_TRANSITION.JUDGMENT_DAY_DISCOVERY: rows(body.judgeA, "judgeA"); rows(body.judgeB, "judgeB"); break;
1023
+ case REVIEW_TRANSITION.ORDINARY_EVIDENCE:
1024
+ results(body.deterministicResults, "deterministicResults");
1025
+ if (body.refuterResults !== undefined) results(body.refuterResults, "refuterResults");
1026
+ break;
1027
+ case REVIEW_TRANSITION.ORDINARY_FIX:
1028
+ case REVIEW_TRANSITION.JUDGMENT_DAY_FIX:
1029
+ string(body.candidateTree, "candidateTree"); strings(body.fixedIds, "fixedIds"); string(body.fixDiff, "fixDiff");
1030
+ if (body.changedPaths !== undefined) strings(body.changedPaths, "changedPaths");
1031
+ break;
1032
+ case REVIEW_TRANSITION.ORDINARY_NO_FIX: string(body.reason, "reason"); break;
1033
+ case REVIEW_TRANSITION.ORDINARY_FINAL_VERIFICATION:
1034
+ case REVIEW_TRANSITION.JUDGMENT_DAY_FINAL_VERIFICATION:
1035
+ boolean(body.passed, "passed"); if (body.reason !== undefined) string(body.reason, "reason"); break;
1036
+ case REVIEW_TRANSITION.ORDINARY_VALIDATION:
1037
+ case REVIEW_TRANSITION.JUDGMENT_DAY_REJUDGMENT: {
1038
+ const request = object(body.request, "request");
1039
+ strings(request.requested_ids, "request.requested_ids"); rows(request.frozen_rows, "request.frozen_rows"); string(request.frozen_ledger_hash, "request.frozen_ledger_hash");
1040
+ if (transition === REVIEW_TRANSITION.JUDGMENT_DAY_REJUDGMENT) {
1041
+ for (const field of ["fix_diff", "fix_diff_hash", "candidate_tree"]) string(request[field], `request.${field}`);
1042
+ if (typeof request.round !== "number" || !Number.isSafeInteger(request.round) || request.round < 1) fail("request.round");
1043
+ results(body.judgeAResults, "judgeAResults"); results(body.judgeBResults, "judgeBResults");
1044
+ } else {
1045
+ results(body.results, "results");
1046
+ const acceptance = object(request.original_acceptance_tests, "request.original_acceptance_tests");
1047
+ boolean(acceptance.passed, "request.original_acceptance_tests.passed"); string(acceptance.evidence_hash, "request.original_acceptance_tests.evidence_hash");
1048
+ strings(request.original_criterion_regressions, "request.original_criterion_regressions");
1049
+ if (!Array.isArray(request.correction_regressions) || !Array.isArray(request.follow_ups)) return fail("request.validation arrays");
1050
+ for (const value of request.correction_regressions) {
1051
+ const regression = object(value, "request.correction_regressions");
1052
+ string(regression.finding_id, "regression.finding_id"); string(regression.evidence_hash, "regression.evidence_hash"); boolean(regression.passed, "regression.passed");
1053
+ }
1054
+ for (const value of request.follow_ups) {
1055
+ const followUp = object(value, "request.follow_ups");
1056
+ for (const field of ["id", "location", "summary", "evidence_hash"]) string(followUp[field], `follow_up.${field}`);
1057
+ }
1058
+ }
1059
+ break;
1060
+ }
1061
+ default: fail("transition");
1062
+ }
1063
+ }
1064
+
997
1065
  function reduceReviewState(
998
1066
  state: ReviewStateV1,
999
1067
  transition: ReviewTransition,
1000
- input: ReviewReducerInput,
1068
+ input: unknown,
1001
1069
  ): ReviewStateV1 {
1070
+ assertReducerInput(transition, input);
1002
1071
  switch (transition) {
1003
1072
  case REVIEW_TRANSITION.ORDINARY_DISCOVERY:
1004
1073
  return recordOrdinaryDiscovery(state, input as OrdinaryDiscoveryInput);
@@ -1456,8 +1525,8 @@ export class ReviewTransactionStore {
1456
1525
  const existing = current ? (current.body.lineages as Array<Record<string, unknown>>).find((value) => value.lineage_id === next.lineage_id && value.mode === "graph") : undefined;
1457
1526
  if (previous && !existing) throw new ReviewIntegrityError("Graph predecessor is missing");
1458
1527
  if (!previous && existing) throw new ReviewIntegrityError("Graph lineage already exists");
1459
- const predecessor = existing?.head_event_id;
1460
- if (predecessor !== undefined && typeof predecessor !== "string") throw new ReviewIntegrityError("Graph head is invalid");
1528
+ if (existing?.head_event_id !== undefined && typeof existing.head_event_id !== "string") throw new ReviewIntegrityError("Graph head is invalid");
1529
+ const predecessor = typeof existing?.head_event_id === "string" ? existing.head_event_id : undefined;
1461
1530
  const last = next.request_journal.at(-1);
1462
1531
  const descriptor = (() => { try { return graph.readStoreDescriptor(); } catch { return undefined; } })();
1463
1532
  const reducerTransition = eventContext?.transition ?? (predecessor === undefined ? "start" : last?.operation ?? "state-update");
@@ -18,7 +18,7 @@ const tokenFields = ["input", "output", "cacheRead", "cacheWrite", "reasoning",
18
18
  * Only the fixed package catalog is cached; runtime instructions are not retained.
19
19
  */
20
20
  let definitions: Array<{ name: string; fingerprint: string; fingerprintClass?: AgentClass }> | undefined;
21
- const packagedAgentClassAliases = new Map([["sdd-proposal", "sdd-propose"]] as const);
21
+ const packagedAgentClassAliases = new Map<string, string>([["sdd-proposal", "sdd-propose"]]);
22
22
  function fingerprintAgentClassName(name: string): string {
23
23
  const compatibilityName = name.startsWith("gentle-ai-") ? name.slice("gentle-ai-".length) : name;
24
24
  return packagedAgentClassAliases.get(compatibilityName) ?? compatibilityName;
@@ -1,5 +1,5 @@
1
1
  import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from "node:child_process";
2
- import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
2
+ import { gentleAiIsolationEnv, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
3
3
  import { runtimeMetricsEnvAllows } from "./runtime-metrics-policy.ts";
4
4
  import { isPublicRuntimeModelId, normalizeRuntimeModel, type RuntimeMetricBucket } from "./runtime-metrics.ts";
5
5
  import type { ChildLaunchBucket } from "./runtime-metrics-children.ts";
@@ -105,7 +105,8 @@ export async function sendNativeRuntimeEvent(rows: readonly RuntimeMetricBucket[
105
105
  const file = (deps.resolve ?? resolveGentleAiBinary)();
106
106
  if (!runtimeMetricsEnvAllows(env)) return "disabled";
107
107
  if (deps.signal?.aborted || deps.current?.() === false) return "discarded";
108
- return await sendProcess(file, cwd, payload, env, deps.spawn ?? spawn, deps.signal);
108
+ // S9: a bundled install keeps Gentle AI's state inside its prefix.
109
+ return await sendProcess(file, cwd, payload, gentleAiIsolationEnv(env), deps.spawn ?? spawn, deps.signal);
109
110
  } catch { return "discarded"; }
110
111
  finally { busy = false; }
111
112
  }