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,9 +1,9 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, isAbsolute, join, posix, win32 } from "node:path";
5
5
  import { promisify } from "node:util";
6
- import { PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
6
+ import { gentleAiIsolationEnv, PackageLocalGentleAiBinaryMissingError, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
7
7
  import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
8
8
  import { decodeReviewAssessmentV1, type ReviewAssessmentV1 } from "./review-risk-assessment.ts";
9
9
  import {
@@ -31,6 +31,8 @@ import {
31
31
  type ReviewLastEventClosureV1,
32
32
  } from "./review-integration-v2.ts";
33
33
 
34
+ import { observeNativeStatus, measureStatusSync, measureStatusAsync } from "./status-timing-diagnostics.ts";
35
+
34
36
  const execFileAsync = promisify(execFile);
35
37
 
36
38
  // Negotiated review/status responses can carry a complete authority inventory.
@@ -250,6 +252,8 @@ export interface NativeReviewReclaimRequest {
250
252
 
251
253
  export interface NativeReviewRecoverRequest {
252
254
  cwd: string;
255
+ baseRef?: string;
256
+ committedOnly?: boolean;
253
257
  predecessorLineage: string;
254
258
  expectedPredecessorRevision: string;
255
259
  successorLineage: string;
@@ -275,6 +279,13 @@ export const NATIVE_REVIEW_LEGACY_ALIAS_REPAIR = {
275
279
  } as const;
276
280
  export type NativeReviewReconcileAnomalies = (typeof NATIVE_REVIEW_RECONCILE_ANOMALIES)[keyof typeof NATIVE_REVIEW_RECONCILE_ANOMALIES];
277
281
 
282
+ export const NATIVE_REVIEW_ABANDON_REASONS = ["operator_disposition", "retired_schema"] as const;
283
+ export type NativeReviewAbandonReason = (typeof NATIVE_REVIEW_ABANDON_REASONS)[number];
284
+
285
+ export function isNativeReviewAbandonReason(value: unknown): value is NativeReviewAbandonReason {
286
+ return value === "operator_disposition" || value === "retired_schema";
287
+ }
288
+
278
289
  export interface NativeReviewAbandonRequest {
279
290
  cwd: string;
280
291
  lineage: string;
@@ -283,7 +294,7 @@ export interface NativeReviewAbandonRequest {
283
294
  capturedLensResults: readonly string[];
284
295
  findingsPresent: boolean;
285
296
  actor: string;
286
- reason: string;
297
+ reason: NativeReviewAbandonReason;
287
298
  maintainerAuthorization: string;
288
299
  signal?: AbortSignal;
289
300
  }
@@ -471,6 +482,24 @@ export interface NativeReviewUnachievableLensCaptureArtifact {
471
482
  // gentle-pi#638 fail-open capability gate: `review capture-unachievable` is younger than every released binary pinned in NATIVE_CLI_CONTRACTS, so the verb is gated invocation-adjacent instead of by a capability row. An older binary renders Go's exact `unknown review command "capture-unachievable"` refusal (internal/cli/review_facade.go) on stderr with no stdout, so the invocation rejects before any decode and the captured diagnostics are the only place that text survives. Every other failure -- a typed binding-mismatch refusal, a timeout, a decode failure -- is a real outcome the caller must surface, never a capability signal. Duck-typed on purpose: the classifier must survive a duplicated module instance exactly like the error it inspects.
472
483
  const NATIVE_REVIEW_UNKNOWN_UNACHIEVABLE_VERB_REFUSAL = /unknown review command "capture-unachievable"/;
473
484
 
485
+ // A provider older than START lens selection refuses the STATUS preflight
486
+ // flags with this exact typed preflight cause, before reading the repository.
487
+ const NATIVE_REVIEW_UNKNOWN_LENS_FLAG_CAUSE = /^flag provided but not defined: -lenses(?:-reason)?$/;
488
+
489
+ /** Reports a not-started STATUS refusal caused only by an unknown --lenses flag. */
490
+ export function isNativeReviewLensSelectionUnsupported(error: unknown): boolean {
491
+ if (!(error instanceof NativeReviewIntegrationError)) return false;
492
+ const failure = error.failureEnvelope;
493
+ return failure.code === "invalid_request" && failure.mutationOutcome === "not_started" &&
494
+ typeof failure.cause === "string" && NATIVE_REVIEW_UNKNOWN_LENS_FLAG_CAUSE.test(failure.cause);
495
+ }
496
+
497
+ function nativeLensSelectionArguments(request: NativeLensSelectionRequest): readonly string[] {
498
+ if (request.lenses === undefined && request.lensesReason === undefined) return [];
499
+ if (request.lenses === undefined || request.lenses.length === 0 || request.lensesReason === undefined) throw new TypeError("Native lens selection requires lenses and lensesReason together");
500
+ return ["--lenses", request.lenses.join(","), "--lenses-reason", request.lensesReason];
501
+ }
502
+
474
503
  export function isNativeReviewUnachievableVerbRefused(error: unknown): boolean {
475
504
  if (typeof error !== "object" || error === null) return false;
476
505
  const stderr = (error as { diagnostics?: { stderr?: unknown } }).diagnostics?.stderr;
@@ -497,7 +526,17 @@ interface NativeUntrackedSelectionRequest {
497
526
  intendedUntrackedSelection?: NativeIntendedUntrackedSelectionSubmission;
498
527
  }
499
528
 
500
- export interface NativeStartRequest extends NativeUntrackedSelectionRequest {
529
+ /**
530
+ * The agent's own review lens choice (gentle-ai START --lenses): canonical
531
+ * lens names in 4R order and one reason. Providers that predate it refuse
532
+ * the flag, and START then falls back to the tier default.
533
+ */
534
+ export interface NativeLensSelectionRequest {
535
+ lenses?: readonly string[];
536
+ lensesReason?: string;
537
+ }
538
+
539
+ export interface NativeStartRequest extends NativeUntrackedSelectionRequest, NativeLensSelectionRequest {
501
540
  cwd: string;
502
541
  baseRef?: string;
503
542
  committedOnly?: boolean;
@@ -524,7 +563,7 @@ export interface NativeReviewConsentDeclinedResult {
524
563
  export interface NativeReviewConsentStartedResult { kind: "started"; start: NativeStartResult; }
525
564
  export type NativeReviewConsentAnswerResult = NativeReviewConsentStartedResult | NativeReviewConsentDeclinedResult;
526
565
  export interface NativeReviewStatusRequest { cwd: string; signal?: AbortSignal; }
527
- export interface NativeTargetStatusRequest extends NativeUntrackedSelectionRequest {
566
+ export interface NativeTargetStatusRequest extends NativeUntrackedSelectionRequest, NativeLensSelectionRequest {
528
567
  cwd: string;
529
568
  lineageId?: string;
530
569
  baseRef?: string;
@@ -752,6 +791,8 @@ const REVIEW_RISK_SUBJECT_BY_CODE: Readonly<Record<string, string>> = Object.fre
752
791
  shell_source: "shell scripting",
753
792
  process_boundary: "code that starts other processes",
754
793
  process_scan_limit: "code that starts other processes",
794
+ dangerous_sink: "a dangerous code pattern",
795
+ agent_escalation: "the agent that made this change flagged it as high risk",
755
796
  executable_mode: "an executable permission change",
756
797
  executable_change: "an executable change",
757
798
  configuration_change: "a configuration change",
@@ -1034,6 +1075,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
1034
1075
  // to /v4 without a review-integration/v2 change. No new negotiated
1035
1076
  // capability is asserted.
1036
1077
  "4.0.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1078
+ // v4.1.1 repeats 4.0.0: the published binary still advertises
1079
+ // capabilities/v2.6 under review-integration/v2. The provider contract moved
1080
+ // from 1.2.0 to 1.3.0 (additive agent lens selection, gentle-ai#5348), with
1081
+ // published tarball SHA-256
1082
+ // b04b6b75d1a446c942a816fbf92932d16f8c3d3001f93159dee2a92bbf2ebe44.
1083
+ // No new negotiated capability identity is asserted.
1084
+ "4.1.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
1037
1085
  });
1038
1086
 
1039
1087
  export interface NativeReviewProcessDiagnostics {
@@ -1075,21 +1123,98 @@ export class NativeReviewCliError extends Error {
1075
1123
  // makes. It declares the Pi host relay handshake on each spawn: gentle-ai
1076
1124
  // refuses pi admission pre-authority without it (gentle-pi#311 P4), and a
1077
1125
  // single injection point keeps the declaration impossible to forget on any
1078
- // individual operation.
1126
+ // individual operation. In a bundled install it also keeps Gentle AI's state
1127
+ // inside the prefix (gentleAiIsolationEnv, S9).
1079
1128
  export function gentleAiProcessEnvironment(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
1080
- return { ...base, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
1129
+ return { ...gentleAiIsolationEnv(base), [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
1130
+ }
1131
+
1132
+ export type ProcessTreeKiller = (pid: number) => void;
1133
+
1134
+ type TreeKillSpawn = (file: string, args: readonly string[], options: { cwd: string; stdio: "ignore"; windowsHide: true }) => { on(event: "error", listener: () => void): unknown };
1135
+
1136
+ // A bare "taskkill" resolves from the current directory before PATH on
1137
+ // Windows, so a repository shipping its own taskkill.exe would run on any
1138
+ // abort. Always use the System32 binary, from System32 itself.
1139
+ export function windowsTaskkillPath(env: NodeJS.ProcessEnv = process.env): string {
1140
+ const systemRoot = env.SystemRoot || env.windir;
1141
+ return win32.join(systemRoot && win32.isAbsolute(systemRoot) ? systemRoot : "C:\\Windows", "System32", "taskkill.exe");
1142
+ }
1143
+
1144
+ // taskkill walks the tree from a live parent, so it must run before the
1145
+ // parent dies; Node's own abort/timeout kill would orphan the git children.
1146
+ export function createWindowsProcessTreeKiller(env: NodeJS.ProcessEnv = process.env, spawnProcess: TreeKillSpawn = spawn): ProcessTreeKiller {
1147
+ const taskkill = windowsTaskkillPath(env);
1148
+ return (pid) => {
1149
+ try {
1150
+ spawnProcess(taskkill, ["/pid", String(pid), "/T", "/F"], { cwd: win32.dirname(taskkill), stdio: "ignore", windowsHide: true }).on("error", () => {});
1151
+ } catch { /* Best effort: the call still settles through its own child. */ }
1152
+ };
1153
+ }
1154
+
1155
+ export const windowsProcessTreeKiller: ProcessTreeKiller = (pid) => createWindowsProcessTreeKiller()(pid);
1156
+
1157
+ const WINDOWS_TREE_KILL_GRACE_MS = 1_000;
1158
+
1159
+ function abortError(signal: AbortSignal): Error {
1160
+ return Object.assign(new Error("The operation was aborted", { cause: signal.reason }), { name: "AbortError", code: "ABORT_ERR" });
1161
+ }
1162
+
1163
+ function execFileResultFromError(error: unknown, timedOut: boolean): ExecFileResult {
1164
+ const detail = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string; code?: string | number; signal?: NodeJS.Signals; killed?: boolean };
1165
+ if (detail.code === "ENOENT" || detail.code === "EACCES" || detail.name === "AbortError") throw error;
1166
+ const outputLimitExceeded = detail.code === "ENOBUFS" || detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
1167
+ return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: !outputLimitExceeded && (timedOut || detail.killed === true), outputLimitExceeded };
1081
1168
  }
1082
1169
 
1083
- export function createNodeExecFileAdapter(): ExecFileAdapter {
1170
+ // Windows: abort and timeout kill the whole process tree instead of only
1171
+ // gentle-ai, with the same observable results as the POSIX path.
1172
+ function windowsExecFile(request: ExecFileRequest, killProcessTree: ProcessTreeKiller): Promise<ExecFileResult> {
1173
+ const { signal } = request;
1174
+ if (signal?.aborted) return Promise.reject(abortError(signal));
1175
+ return new Promise((resolve, reject) => {
1176
+ let timedOut = false, aborted = false, pipesCut = false;
1177
+ let timer: ReturnType<typeof setTimeout> | undefined;
1178
+ let fallback: ReturnType<typeof setTimeout> | undefined;
1179
+ const child = execFile(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, maxBuffer: request.maxBufferBytes, env: gentleAiProcessEnvironment() }, (error, stdout, stderr) => {
1180
+ if (timer !== undefined) clearTimeout(timer);
1181
+ if (fallback !== undefined) clearTimeout(fallback);
1182
+ signal?.removeEventListener("abort", onAbort);
1183
+ if (aborted && signal) { reject(abortError(signal)); return; }
1184
+ if (error === null) { resolve({ stdout, stderr, exitCode: 0, signal: null, timedOut: pipesCut, outputLimitExceeded: false }); return; }
1185
+ try { resolve(execFileResultFromError(Object.assign(error, { stdout, stderr }), timedOut)); } catch (failure) { reject(failure); }
1186
+ });
1187
+ // If taskkill cannot run (AppLocker, a broken PATH), still end gentle-ai
1188
+ // itself so the deadline holds; its children may then outlive it. A
1189
+ // surviving descendant can still hold the inherited stdout/stderr open,
1190
+ // which would keep execFile from calling back, so cut the pipes too.
1191
+ const killTree = () => {
1192
+ if (child.pid === undefined) { child.kill(); return; }
1193
+ killProcessTree(child.pid);
1194
+ fallback = setTimeout(() => {
1195
+ if (child.exitCode === null && child.signalCode === null) child.kill();
1196
+ pipesCut = true;
1197
+ child.stdout?.destroy();
1198
+ child.stderr?.destroy();
1199
+ }, WINDOWS_TREE_KILL_GRACE_MS);
1200
+ };
1201
+ function onAbort(): void { if (aborted || timedOut) return; aborted = true; killTree(); }
1202
+ signal?.addEventListener("abort", onAbort, { once: true });
1203
+ if (request.timeoutMs !== undefined && request.timeoutMs > 0) timer = setTimeout(() => { if (aborted) return; timedOut = true; killTree(); }, request.timeoutMs);
1204
+ });
1205
+ }
1206
+
1207
+ export function createNodeExecFileAdapter(options: { readonly platform?: NodeJS.Platform; readonly killProcessTree?: ProcessTreeKiller } = {}): ExecFileAdapter {
1208
+ if ((options.platform ?? process.platform) === "win32") {
1209
+ const killProcessTree = options.killProcessTree ?? windowsProcessTreeKiller;
1210
+ return (request) => windowsExecFile(request, killProcessTree);
1211
+ }
1084
1212
  return async (request) => {
1085
1213
  try {
1086
1214
  const output = await execFileAsync(request.file, [...request.arguments], { cwd: request.cwd, encoding: "utf8", shell: false, windowsHide: true, timeout: request.timeoutMs, maxBuffer: request.maxBufferBytes, signal: request.signal, env: gentleAiProcessEnvironment() });
1087
1215
  return { stdout: output.stdout, stderr: output.stderr, exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
1088
1216
  } catch (error) {
1089
- const detail = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string; code?: string | number; signal?: NodeJS.Signals; killed?: boolean };
1090
- if (detail.code === "ENOENT" || detail.code === "EACCES" || detail.name === "AbortError") throw error;
1091
- const outputLimitExceeded = detail.code === "ENOBUFS" || detail.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
1092
- return { stdout: detail.stdout ?? "", stderr: detail.stderr ?? "", exitCode: typeof detail.code === "number" ? detail.code : 1, signal: detail.signal ?? null, timedOut: !outputLimitExceeded && detail.killed === true, outputLimitExceeded };
1217
+ return execFileResultFromError(error, false);
1093
1218
  }
1094
1219
  };
1095
1220
  }
@@ -1186,8 +1311,10 @@ function nativeProcessDiagnostics(operation: NativeReviewOperation, code: Native
1186
1311
  }
1187
1312
 
1188
1313
  function parseJson(stdout: string, operation: NativeReviewOperation, mutating: boolean, diagnostics: NativeReviewProcessDiagnostics): Record<string, unknown> {
1189
- if (stdout.length === 0) throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT, operation, true, mutating, "native command returned empty output", { ...diagnostics, error_code: NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT });
1190
- try { return object(JSON.parse(stdout)); } catch { throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON, operation, true, mutating, "native command returned malformed JSON", { ...diagnostics, error_code: NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON }); }
1314
+ return measureStatusSync("decode", () => {
1315
+ if (stdout.length === 0) throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT, operation, true, mutating, "native command returned empty output", { ...diagnostics, error_code: NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT });
1316
+ try { return object(JSON.parse(stdout)); } catch { throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON, operation, true, mutating, "native command returned malformed JSON", { ...diagnostics, error_code: NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON }); }
1317
+ });
1191
1318
  }
1192
1319
  function decodeNativeMaintenanceResult(value: unknown, expectedOperation: NativeReviewOperation): NativeReviewRecoveryResult {
1193
1320
  const body = exactObject(value, ["operation", "record"]);
@@ -1218,7 +1345,7 @@ function assertSupportedNextTransitionOperation(body: Record<string, unknown>):
1218
1345
  }
1219
1346
  }
1220
1347
  function decode<T>(operation: NativeReviewOperation, mutating: boolean, callback: () => T, diagnostics = nativeProcessDiagnostics(operation, NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE)): T {
1221
- try { return callback(); } catch (error) { if (error instanceof NativeReviewCliError) throw error; throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, operation, true, mutating, "native response is schema incompatible", { ...diagnostics, error_code: NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE }); }
1348
+ try { return measureStatusSync("decode", callback); } catch (error) { if (error instanceof NativeReviewCliError) throw error; throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE, operation, true, mutating, "native response is schema incompatible", { ...diagnostics, error_code: NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE }); }
1222
1349
  }
1223
1350
  function decodeReviewStartResponse(value: unknown): ReviewStartV3 | ReviewStartV4 {
1224
1351
  const body = object(value);
@@ -1231,50 +1358,6 @@ function decodeReleaseEvidence(value: unknown): void {
1231
1358
  for (const field of ["release_tree", "configuration_hash", "generated_artifact_hash", "provenance_hash", "publication_boundary_hash", "evidence_freshness_hash"]) requiredString(release[field]);
1232
1359
  if (release.publication_state !== "sealed" || release.evidence_freshness_state !== "current") throw new Error("invalid release evidence");
1233
1360
  }
1234
- function decodeNonDecidingGateContext(value: unknown, expectedGate: string): NativeGateContext {
1235
- const context = exactObject(value, ["gate"]);
1236
- const gate = enumString(context.gate, NATIVE_GATE);
1237
- if (gate !== expectedGate) throw new Error("native non-deciding gate context does not match the requested gate");
1238
- return { lineageId: "", storeRevision: "", raw: context };
1239
- }
1240
- function decodeGateContext(value: unknown): NativeGateContext {
1241
- const context = exactObject(
1242
- value,
1243
- ["gate", "lineage_id", "generation", "base_tree", "candidate_tree", "paths_digest", "fix_delta_hash", "policy_hash", "ledger_hash", "evidence_hash", "base_relationship_valid"],
1244
- ["store_revision", "genesis_revision", "chain_identity", "bundle_digest", "external_evidence", "base_advanced_compatible", "release", "pre_pr_boundary", "denial"],
1245
- );
1246
- const gate = stringValue(context.gate);
1247
- if (gate !== "" && !(NATIVE_GATE as readonly string[]).includes(gate)) throw new Error("invalid gate context gate");
1248
- for (const field of ["lineage_id", "base_tree", "candidate_tree", "paths_digest", "fix_delta_hash", "policy_hash", "ledger_hash", "evidence_hash"]) stringValue(context[field]);
1249
- for (const field of ["store_revision", "genesis_revision", "chain_identity", "bundle_digest"]) if (context[field] !== undefined) stringValue(context[field]);
1250
- nonNegativeInteger(context.generation);
1251
- booleanValue(context.base_relationship_valid);
1252
- if (context.external_evidence !== undefined) enumString(context.external_evidence, ["invalidating", "escalating"]);
1253
- let sanitizedContext = context;
1254
- if (context.denial !== undefined) {
1255
- const denial = exactObject(context.denial, ["stage", "code"]);
1256
- const stage = sanitizeNativeDiagnosticText(requiredString(denial.stage), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1257
- const code = sanitizeNativeDiagnosticText(requiredString(denial.code), NATIVE_REVIEW_DENIAL_TEXT_LIMIT);
1258
- if (!isCanonicalProcessString(stage) || !isCanonicalProcessString(code)) throw new Error("non-canonical denial evidence");
1259
- sanitizedContext = { ...context, denial: { stage, code } };
1260
- }
1261
- if (context.pre_pr_boundary !== undefined) {
1262
- const boundary = exactObject(context.pre_pr_boundary, ["source", "selector", "commit"], ["remote", "remote_ref", "remote_identity"]);
1263
- enumString(boundary.source, ["explicit", "publication-default"]); requiredString(boundary.selector); stringValue(boundary.commit);
1264
- for (const field of ["remote", "remote_ref", "remote_identity"]) if (boundary[field] !== undefined) requiredString(boundary[field]);
1265
- }
1266
- if (context.base_advanced_compatible !== undefined) {
1267
- const proof = exactObject(context.base_advanced_compatible, ["status", "compatible", "old_base_tree", "new_base_tree", "original_patch_identity", "delivered_patch_identity", "delivered_paths_digest", "base_advance_paths_digest", "paths_disjoint", "merged_result_tree", "ci_attestation_artifact_hash", "ci_attestation_issuer", "ci_status"]);
1268
- for (const field of ["status", "old_base_tree", "new_base_tree", "original_patch_identity", "delivered_patch_identity", "delivered_paths_digest", "base_advance_paths_digest", "merged_result_tree", "ci_attestation_artifact_hash", "ci_attestation_issuer", "ci_status"]) requiredString(proof[field]);
1269
- booleanValue(proof.compatible); booleanValue(proof.paths_disjoint);
1270
- }
1271
- if (context.release !== undefined) decodeReleaseEvidence(context.release);
1272
- return {
1273
- lineageId: stringValue(context.lineage_id),
1274
- storeRevision: context.store_revision === undefined ? "" : stringValue(context.store_revision),
1275
- raw: sanitizedContext,
1276
- };
1277
- }
1278
1361
  function decodeNativeReviewRecovery(value: unknown): NativeReviewRecovery {
1279
1362
  const recovery = exactObject(value, ["predecessor_lineage_id", "predecessor_revision", "disposition", "reason", "actor", "recovered_at"], ["maintainer_authorization"]);
1280
1363
  return {
@@ -1365,7 +1448,7 @@ function decodeNativeReviewStatus(value: unknown): NativeReviewStatusResult {
1365
1448
  const complete = booleanValue(body.complete);
1366
1449
  const authoritative = booleanValue(body.authoritative);
1367
1450
  if (authoritative && !complete) throw new Error("incomplete inventory cannot be authoritative");
1368
- if (!Array.isArray(body.entries) || !Array.isArray(body.locks)) throw new Error("invalid native status inventory");
1451
+ if (!Array.isArray(body.entries) || !Array.isArray(body.locks) || !Array.isArray(body.diagnostics)) throw new Error("invalid native status inventory");
1369
1452
  return {
1370
1453
  repository: requiredString(body.repository),
1371
1454
  complete,
@@ -1537,7 +1620,10 @@ class NativeReviewPlainCli {
1537
1620
  // membership in the frozen set.
1538
1621
  private async execute(operation: NativeReviewOperation, cwd: string, arguments_: readonly string[], mutating: boolean, signal?: AbortSignal, toleratedStderr: readonly string[] = []): Promise<NativeJsonExecution> {
1539
1622
  let result: ExecFileResult;
1540
- try { result = await this.adapter({ file: this.executablePath(operation, mutating), arguments: arguments_, cwd, timeoutMs: mutating ? undefined : this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }); }
1623
+ try {
1624
+ const file = measureStatusSync("resolution", () => this.executablePath(operation, mutating));
1625
+ result = await measureStatusAsync("adapter", () => this.adapter({ file, arguments: arguments_, cwd, timeoutMs: mutating ? undefined : this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }));
1626
+ }
1541
1627
  catch (error) {
1542
1628
  if (error instanceof NativeReviewCliError) throw nativeError(error.code, operation, mutating, error.message, undefined, error.launchAttempted);
1543
1629
  if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, operation, mutating, "native process was cancelled");
@@ -1547,7 +1633,7 @@ class NativeReviewPlainCli {
1547
1633
  if (result.outputLimitExceeded) throw nativeError(NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT, operation, mutating, "native process output exceeded limit", result, true, undefined, this.maxBufferBytes);
1548
1634
  if (result.timedOut) throw nativeError(NATIVE_REVIEW_ERROR_CODE.TIMEOUT, operation, mutating, "native process timed out", result);
1549
1635
  if (result.signal) throw nativeError(NATIVE_REVIEW_ERROR_CODE.SIGNAL, operation, mutating, "native process was signalled", result);
1550
- const maintenancePartialFailure = [NATIVE_REVIEW_OPERATION.ABANDON, NATIVE_REVIEW_OPERATION.QUARANTINE_LEGACY, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, NATIVE_REVIEW_OPERATION.REPAIR_LEGACY_ALIAS].includes(operation) && result.exitCode !== 0;
1636
+ const maintenancePartialFailure = new Set<NativeReviewOperation>([NATIVE_REVIEW_OPERATION.ABANDON, NATIVE_REVIEW_OPERATION.QUARANTINE_LEGACY, NATIVE_REVIEW_OPERATION.RECONCILE_AUTHORITY, NATIVE_REVIEW_OPERATION.REPAIR_LEGACY_ALIAS]).has(operation) && result.exitCode !== 0;
1551
1637
  const toleratedNotice = stderrIsTolerated(result.stderr, toleratedStderr);
1552
1638
  if (result.exitCode !== 0 && !maintenancePartialFailure) throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native process failed", result);
1553
1639
  if (result.stderr.trim().length > 0 && !maintenancePartialFailure && !toleratedNotice) throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR, operation, mutating, "native process wrote stderr", result);
@@ -1555,6 +1641,10 @@ class NativeReviewPlainCli {
1555
1641
  }
1556
1642
 
1557
1643
  async reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
1644
+ return observeNativeStatus(this.timeoutMs, () => this.observedReviewStatus(request));
1645
+ }
1646
+
1647
+ private async observedReviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
1558
1648
  const { body: result } = await this.execute(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, ["review", "status", "--cwd", request.cwd], false, request.signal);
1559
1649
  const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(result));
1560
1650
  if (!await repositoriesMatch(request.cwd, status.repository)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.STATUS, false, "native review status repository mismatch");
@@ -1602,9 +1692,13 @@ class NativeReviewPlainCli {
1602
1692
  if (request.maintainerAuthorization !== undefined && (request.maintainerAuthorization.length === 0 || /[\u0000-\u0009\u000b-\u001f\u007f]/.test(request.maintainerAuthorization))) {
1603
1693
  throw new TypeError("Native RECOVER maintainerAuthorization must be a non-empty LF-only binding");
1604
1694
  }
1695
+ if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native RECOVER baseRef must be a non-empty, trimmed, NUL-free string");
1696
+ if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native RECOVER baseRef requires explicit committedOnly acknowledgement");
1697
+ if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native RECOVER committedOnly requires an explicit baseRef");
1605
1698
  if (!(NATIVE_REVIEW_RECOVER_DISPOSITION as readonly string[]).includes(request.disposition)) throw new TypeError("Native RECOVER disposition must be scope_changed, invalidated, or escalated");
1606
1699
  const { body } = await this.execute(NATIVE_REVIEW_OPERATION.RECOVER, request.cwd, [
1607
1700
  "review", "recover", "--cwd", request.cwd,
1701
+ ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
1608
1702
  "--predecessor-lineage", request.predecessorLineage,
1609
1703
  "--expected-predecessor-revision", request.expectedPredecessorRevision,
1610
1704
  "--successor-lineage", request.successorLineage,
@@ -1617,6 +1711,7 @@ class NativeReviewPlainCli {
1617
1711
  }
1618
1712
 
1619
1713
  async abandon(request: NativeReviewAbandonRequest): Promise<NativeReviewRecoveryResult> {
1714
+ if (!isNativeReviewAbandonReason(request.reason)) throw new TypeError('Native ABANDON reason must be "operator_disposition" or "retired_schema"');
1620
1715
  for (const [name, value] of [["lineage", request.lineage], ["expectedRevision", request.expectedRevision], ["snapshotIdentity", request.snapshotIdentity], ["actor", request.actor], ["reason", request.reason]] as const) {
1621
1716
  if (!isCanonicalProcessString(value)) throw new TypeError(`Native ABANDON ${name} must be a non-empty, trimmed, NUL-free string`);
1622
1717
  }
@@ -1715,6 +1810,10 @@ export function nativeReviewAbandonAuthorization(request: Pick<NativeReviewAband
1715
1810
  // so the native v2 gate verifies an exact eight-line binding (schema, lineage,
1716
1811
  // revision, snapshot_identity, reason, captured_lens_results, findings_present,
1717
1812
  // actor) — there is no evidence_records_present line to derive or relay.
1813
+ // capturedLensResults must arrive verbatim from the native authority
1814
+ // inventory projection (issue #1159): the gate recomputes this line from its
1815
+ // own ordered record, whose entries carry the ordinal prefix, so caller- or
1816
+ // facade-authored lens names cannot reproduce it.
1718
1817
  return [
1719
1818
  "gentle-ai.review-abandon-authorization/v2",
1720
1819
  `lineage=${request.lineage}`,
@@ -1855,10 +1954,11 @@ function splitNativeConsentInvocation(invocation: string): readonly string[] {
1855
1954
  continue;
1856
1955
  }
1857
1956
  // Provider invocations are not shell commands. Keep every path backslash
1858
- // verbatim, including quoted UNC and drive-root paths. Outside quotes,
1859
- // only a backslash before whitespace joins a space-containing token.
1957
+ // verbatim, including quoted UNC and drive-root paths. Outside quotes, a
1958
+ // backslash escapes only whitespace or a quote, so a space-containing token
1959
+ // and a POSIX-quoted apostrophe (`'it'\''s'`) split exactly.
1860
1960
  const next = source[index + 1];
1861
- if (character === "\\" && quote === undefined && next !== undefined && /\s/.test(next)) {
1961
+ if (character === "\\" && quote === undefined && next !== undefined && (/\s/.test(next) || next === "'" || next === '"')) {
1862
1962
  escaping = true;
1863
1963
  started = true;
1864
1964
  continue;
@@ -2101,7 +2201,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2101
2201
  ): Promise<NegotiatedExecution> {
2102
2202
  let result: ExecFileResult;
2103
2203
  try {
2104
- result = await this.adapter({ file: path, arguments: arguments_, cwd, timeoutMs: mutating ? undefined : this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal });
2204
+ result = await measureStatusAsync("adapter", () => this.adapter({ file: path, arguments: arguments_, cwd, timeoutMs: mutating ? undefined : this.timeoutMs, maxBufferBytes: this.maxBufferBytes, signal }));
2105
2205
  } catch (error) {
2106
2206
  if (error instanceof Error && error.name === "AbortError") throw nativeError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, operation, mutating, "native process was cancelled");
2107
2207
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.UNAVAILABLE, operation, mutating, "native process could not start");
@@ -2117,7 +2217,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2117
2217
  const body = parseJson(result.stdout, operation, mutating, diagnostics);
2118
2218
  if (result.exitCode !== 0) {
2119
2219
  try {
2120
- throw new NativeReviewIntegrationError(decodeReviewFailureV2(body));
2220
+ throw new NativeReviewIntegrationError(measureStatusSync("decode", () => decodeReviewFailureV2(body)));
2121
2221
  } catch (error) {
2122
2222
  if (error instanceof NativeReviewIntegrationError) throw error;
2123
2223
  throw nativeError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, operation, mutating, "native negotiated operation failed without a valid failure envelope", result);
@@ -2136,7 +2236,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2136
2236
  signal?: AbortSignal,
2137
2237
  toleratedStderr: readonly string[] = [],
2138
2238
  ): Promise<NegotiatedExecution> {
2139
- return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
2239
+ return this.invoke(operation, cwd, arguments_, mutating, signal, measureStatusSync("resolution", () => this.executablePath(operation, mutating)), toleratedStderr);
2140
2240
  }
2141
2241
 
2142
2242
  async start(request: NativeStartRequest): Promise<NativeStartResult> {
@@ -2149,7 +2249,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2149
2249
  // drift; Pi never rebuilds that vector from request fields.
2150
2250
  const projection = request.projection ?? "workspace";
2151
2251
  const selection = nativeUntrackedSelection(request);
2152
- const status = await this.targetStatus({
2252
+ const statusRequest: NativeTargetStatusRequest = {
2153
2253
  cwd: request.cwd,
2154
2254
  projection,
2155
2255
  ...(request.baseRef === undefined ? {} : { baseRef: request.baseRef, committedOnly: true }),
@@ -2158,7 +2258,19 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2158
2258
  ...(request.intendedUntrackedSelection === undefined ? {} : { intendedUntrackedSelection: request.intendedUntrackedSelection }),
2159
2259
  agent: "pi",
2160
2260
  ...(request.signal === undefined ? {} : { signal: request.signal }),
2161
- });
2261
+ };
2262
+ const lensSelection = request.lenses === undefined && request.lensesReason === undefined
2263
+ ? {}
2264
+ : { lenses: request.lenses, lensesReason: request.lensesReason };
2265
+ let status: ReviewStatusV3;
2266
+ try {
2267
+ status = await this.targetStatus({ ...statusRequest, ...lensSelection });
2268
+ } catch (error) {
2269
+ // Phase A: a provider that predates lens selection still reviews,
2270
+ // with the tier default, instead of refusing the candidate.
2271
+ if (Object.keys(lensSelection).length === 0 || !isNativeReviewLensSelectionUnsupported(error)) throw error;
2272
+ status = await this.targetStatus(statusRequest);
2273
+ }
2162
2274
  const transition = status.nextTransition?.kind === "execute" && status.nextTransition.execute?.operation === "review.start"
2163
2275
  ? status.nextTransition.execute
2164
2276
  : undefined;
@@ -2255,6 +2367,10 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2255
2367
  }
2256
2368
 
2257
2369
  async targetStatus(request: NativeTargetStatusRequest): Promise<ReviewStatusV3> {
2370
+ return observeNativeStatus(this.timeoutMs, () => this.observedTargetStatus(request));
2371
+ }
2372
+
2373
+ private async observedTargetStatus(request: NativeTargetStatusRequest): Promise<ReviewStatusV3> {
2258
2374
  if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native STATUS baseRef must be a non-empty, trimmed, NUL-free string");
2259
2375
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native STATUS baseRef requires explicit committedOnly acknowledgement");
2260
2376
  if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native STATUS committedOnly requires an explicit baseRef");
@@ -2293,10 +2409,11 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2293
2409
  ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]),
2294
2410
  ...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
2295
2411
  ...(request.agent === undefined ? [] : ["--agent", request.agent]),
2412
+ ...nativeLensSelectionArguments(request),
2296
2413
  "--next-transition",
2297
- ] : ["review", "status", "--cwd", request.cwd, ...submittedTokens, ...forwardedBaseRef, ...forwardedLineage];
2414
+ ] : ["review", "status", "--cwd", request.cwd, ...submittedTokens, ...forwardedBaseRef, ...forwardedLineage, ...nativeLensSelectionArguments(request)];
2298
2415
  const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, statusArguments, false, request.signal);
2299
- assertSupportedNextTransitionOperation(execution.body);
2416
+ measureStatusSync("decode", () => assertSupportedNextTransitionOperation(execution.body));
2300
2417
  return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV3(execution.body));
2301
2418
  }
2302
2419
 
@@ -2485,13 +2602,17 @@ export class NativeReviewCliV216 implements NativeReviewCli {
2485
2602
  // mutually exclusive with a path.
2486
2603
 
2487
2604
  async reviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
2605
+ return observeNativeStatus(this.timeoutMs, () => this.observedReviewStatus(request));
2606
+ }
2607
+
2608
+ private async observedReviewStatus(request: NativeReviewStatusRequest): Promise<NativeReviewStatusResult> {
2488
2609
  const execution = await this.invoke(
2489
2610
  NATIVE_REVIEW_OPERATION.STATUS,
2490
2611
  request.cwd,
2491
2612
  ["review", "status", "--cwd", request.cwd],
2492
2613
  false,
2493
2614
  request.signal,
2494
- this.executablePath(NATIVE_REVIEW_OPERATION.STATUS, false),
2615
+ measureStatusSync("resolution", () => this.executablePath(NATIVE_REVIEW_OPERATION.STATUS, false)),
2495
2616
  );
2496
2617
  const status = decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeNativeReviewStatus(execution.body));
2497
2618
  if (!await repositoriesMatch(request.cwd, status.repository)) throw nativeError(NATIVE_REVIEW_ERROR_CODE.IDENTITY_MISMATCH, NATIVE_REVIEW_OPERATION.STATUS, false, "native review status repository mismatch");