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
@@ -29,6 +29,7 @@ import {
29
29
  type NativeReviewOutcome,
30
30
  } from "../lib/review-risk-assessment.ts";
31
31
  import { consumeReviewMutation, pendingReviewMutation, recordReviewMutation } from "../lib/review-reminder-receipt.ts";
32
+ import type { ReviewStatusV3 } from "../lib/review-integration-v2.ts";
32
33
 
33
34
  // ---------------------------------------------------------------------------
34
35
  // gentle-pi#662: decoder for the native `gentle-ai review assess` envelope
@@ -188,9 +189,9 @@ test("decodeReviewAssessmentV1 rejects malformed consumed, review_due, and revie
188
189
 
189
190
  // gentle-pi#1175 (T2): the native schema fixes review_due by reason
190
191
  // (true for high_risk and slice_budget_reached, false for passive,
191
- // under_budget, and already_reviewed), and already_reviewed is reported
192
- // exactly when the candidate is consumed. A contradictory envelope can never
193
- // be trusted as closure evidence, so it fails decoding.
192
+ // under_budget, and already_reviewed). Consumed candidates must report
193
+ // already_reviewed, but an acknowledged predecessor plus a passive delta can
194
+ // also produce that reason without consuming this exact candidate (#1954).
194
195
  interface ReviewDuePair {
195
196
  reason: string;
196
197
  due: boolean;
@@ -203,6 +204,7 @@ const CONSISTENT_REVIEW_DUE_PAIRS: readonly ReviewDuePair[] = [
203
204
  { reason: "passive", due: false, consumed: false },
204
205
  { reason: "under_budget", due: false, consumed: false },
205
206
  { reason: "already_reviewed", due: false, consumed: true },
207
+ { reason: "already_reviewed", due: false, consumed: false },
206
208
  ];
207
209
 
208
210
  function consistentEnvelope(pair: ReviewDuePair): Record<string, unknown> {
@@ -237,14 +239,38 @@ test("decodeReviewAssessmentV1 rejects a review_due boolean that contradicts rev
237
239
  }
238
240
  });
239
241
 
240
- test("decodeReviewAssessmentV1 rejects already_reviewed unless the candidate is consumed", () => {
241
- for (const candidate of [{ kind: "current-changes", consumed: false }, { kind: "current-changes" }]) {
242
- assert.throws(
243
- () => decodeReviewAssessmentV1(newEnvelope({ candidate, review_due: false, review_due_reason: "already_reviewed", next_transition: undefined })),
244
- TypeError,
245
- `already_reviewed with ${JSON.stringify(candidate)} must be rejected`,
246
- );
247
- }
242
+ function reviewedPredecessorEnvelope(): Record<string, unknown> {
243
+ // The executable range still has medium risk; only the delta after the
244
+ // acknowledged committed predecessor is a passive ODD note.
245
+ return validEnvelope({
246
+ risk: "medium",
247
+ reasons: [{ code: "executable_changes" }],
248
+ changed_paths: 3,
249
+ changed_lines: 24,
250
+ candidate: { kind: "current-changes", consumed: false },
251
+ review_due: false,
252
+ review_due_reason: "already_reviewed",
253
+ });
254
+ }
255
+
256
+ test("decodeReviewAssessmentV1 accepts an already-reviewed predecessor without consuming the medium-risk candidate (#1954)", () => {
257
+ const decoded = decodeReviewAssessmentV1(reviewedPredecessorEnvelope());
258
+ assert.equal(decoded.risk, "medium");
259
+ assert.deepEqual(decoded.reasons, [{ code: "executable_changes" }]);
260
+ assert.equal(decoded.changedPaths, 3);
261
+ assert.equal(decoded.changedLines, 24);
262
+ assert.equal(decoded.reviewDue, false);
263
+ assert.equal(decoded.reviewDueReason, "already_reviewed");
264
+ assert.equal(decoded.candidate.consumed, false);
265
+ assert.equal(Object.hasOwn(decoded, "nextTransition"), false);
266
+ });
267
+
268
+ test("decodeReviewAssessmentV1 rejects already_reviewed with missing consumed evidence", () => {
269
+ // Native v2 requires consumed; older envelopes omit the entire due pair.
270
+ assert.throws(
271
+ () => decodeReviewAssessmentV1(newEnvelope({ candidate: { kind: "current-changes" }, review_due: false, review_due_reason: "already_reviewed", next_transition: undefined })),
272
+ TypeError,
273
+ );
248
274
  });
249
275
 
250
276
  test("decodeReviewAssessmentV1 rejects a consumed candidate reported with any reason other than already_reviewed", () => {
@@ -311,24 +337,22 @@ const RDD_LINES: readonly RddLine[] = [RDD_LINE.ON, RDD_LINE.OFF, RDD_LINE.UNKNO
311
337
  const PROFILES: readonly WriterProfile[] = [WRITER_PROFILE.SMALL, WRITER_PROFILE.LARGE];
312
338
  const NON_CLOSED_OUTCOMES: readonly NativeReviewOutcome[] = [NATIVE_REVIEW_OUTCOME.DECLINED, NATIVE_REVIEW_OUTCOME.UNAVAILABLE, NATIVE_REVIEW_OUTCOME.UNKNOWN];
313
339
 
314
- test("verificationPlan: rdd on + closed, passive risk -> structural readback only regardless of writer profile", () => {
315
- for (const writerProfile of PROFILES) {
316
- const plan = verificationPlan({ rddLine: RDD_LINE.ON, risk: VERIFICATION_TIER.PASSIVE, writerProfile, nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.CLOSED });
317
- assert.equal(plan.structuralReadbackOnly, true);
318
- assert.equal(plan.writerSelfVerification, false);
319
- assert.equal(plan.independentVerifier, false);
320
- }
321
- });
322
-
323
- test("verificationPlan: rdd on + closed, medium/high/unassessable risk -> writer self-verification, no independent verifier", () => {
324
- for (const risk of [VERIFICATION_TIER.MEDIUM, VERIFICATION_TIER.HIGH, VERIFICATION_TIER.UNASSESSABLE]) {
340
+ // verify-always-rdd-high S1: the native review is an additional outside view
341
+ // and never replaces verification, so a closed review under rdd on yields the
342
+ // exact risk-gated plan rdd off yields, for every risk and writer profile.
343
+ test("verificationPlan: rdd on + closed behaves exactly like rdd off for every risk/profile combination", () => {
344
+ for (const risk of RISKS) {
325
345
  for (const writerProfile of PROFILES) {
326
- const plan = verificationPlan({ rddLine: RDD_LINE.ON, risk, writerProfile, nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.CLOSED });
327
- assert.equal(plan.writerSelfVerification, true, `rdd on+closed, risk ${risk}, profile ${writerProfile}`);
328
- assert.equal(plan.structuralReadbackOnly, false);
329
- assert.equal(plan.independentVerifier, false, `the closed native review is the independent check under rdd on for risk ${risk}`);
346
+ const off = verificationPlan({ rddLine: RDD_LINE.OFF, risk, writerProfile });
347
+ const closed = verificationPlan({ rddLine: RDD_LINE.ON, risk, writerProfile, nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.CLOSED });
348
+ assert.equal(closed.writerSelfVerification, off.writerSelfVerification, `closed, risk ${risk}, profile ${writerProfile}`);
349
+ assert.equal(closed.structuralReadbackOnly, off.structuralReadbackOnly, `closed, risk ${risk}, profile ${writerProfile}`);
350
+ assert.equal(closed.independentVerifier, off.independentVerifier, `closed, risk ${risk}, profile ${writerProfile}`);
330
351
  }
331
352
  }
353
+ const high = verificationPlan({ rddLine: RDD_LINE.ON, risk: VERIFICATION_TIER.HIGH, writerProfile: WRITER_PROFILE.LARGE, nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.CLOSED });
354
+ assert.equal(high.independentVerifier, true, "a closed review never removes the high-risk independent verifier");
355
+ assert.match(high.reason, /never replaces verification/);
332
356
  });
333
357
 
334
358
  // ---------------------------------------------------------------------------
@@ -365,9 +389,9 @@ test("verificationPlan: an omitted nativeReviewOutcome under rdd on defaults to
365
389
  }
366
390
  });
367
391
 
368
- test("verificationPlan: on+closed+medium+large -> no verifier", () => {
369
- const plan = verificationPlan({ rddLine: RDD_LINE.ON, risk: VERIFICATION_TIER.MEDIUM, writerProfile: WRITER_PROFILE.LARGE, nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.CLOSED });
370
- assert.equal(plan.independentVerifier, false);
392
+ test("verificationPlan: on+closed+medium+small -> verifier (the small-model bias still applies)", () => {
393
+ const plan = verificationPlan({ rddLine: RDD_LINE.ON, risk: VERIFICATION_TIER.MEDIUM, writerProfile: WRITER_PROFILE.SMALL, nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.CLOSED });
394
+ assert.equal(plan.independentVerifier, true);
371
395
  assert.equal(plan.writerSelfVerification, true);
372
396
  });
373
397
 
@@ -712,7 +736,25 @@ function assessOnNativeCli(currentTargetIdentity: () => string): Partial<NativeR
712
736
  return {
713
737
  reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: "on", cloneLocal: "", effective: "on", source: NATIVE_REVIEW_MODE_SOURCE.GLOBAL } }),
714
738
  assess: async () => ({ schema: REVIEW_ASSESSMENT_SCHEMA, risk: "high", reasons: [], changedPaths: 1, changedLines: 5, candidate: { kind: "current-changes", baseRef: undefined } }),
715
- targetStatus: (async () => ({ applicability: "current_target", targetIdentity: currentTargetIdentity() })) as NativeReviewCli["targetStatus"],
739
+ targetStatus: async (): Promise<ReviewStatusV3> => ({
740
+ contract: "gentle-ai.review-integration/v2", applicability: "current_target",
741
+ action: "stop", replayability: "manual_action_required", targetIdentity: currentTargetIdentity(),
742
+ projection: {
743
+ schema: "gentle-ai.review-integration.projection/v1",
744
+ kind: "current-changes", projection: "workspace", baseTree: "a".repeat(40),
745
+ initialReviewTree: "a".repeat(40), currentCandidateTree: "b".repeat(40),
746
+ pathsDigest: "c".repeat(64), paths: ["candidate.ts"], intendedUntracked: [],
747
+ intendedUntrackedProof: "d".repeat(64), initialSnapshotIdentity: "e".repeat(64),
748
+ currentSnapshotIdentity: currentTargetIdentity(),
749
+ },
750
+ repair: {
751
+ schema: "gentle-ai.review-authority-repair-assessment/v1", status: "unsupported",
752
+ counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 },
753
+ supportedOperations: ["review/complete-fix", "review/validate-fix"],
754
+ authorizationSchema: "gentle-ai.review-repair-authorization/v1",
755
+ },
756
+ candidates: [], raw: {},
757
+ }),
716
758
  };
717
759
  }
718
760
 
@@ -806,12 +848,46 @@ async function assessWith(cli: Partial<NativeReviewCli>, input?: Record<string,
806
848
  return (await reviewControllerTool(cli).execute("closure", params, undefined, undefined, context)).details as AssessDetails;
807
849
  }
808
850
 
851
+ test("gentle_review assess: already-reviewed predecessor remains medium and never proves exact-candidate closure (#1954)", async (t) => {
852
+ t.after(() => __testing.clearNativeReviewOutcomeMemoForTesting());
853
+ __testing.clearNativeReviewOutcomeMemoForTesting();
854
+ for (const input of [undefined, { nativeReviewOutcome: "closed" }]) {
855
+ const queue = queuedAdapter([{ stdout: JSON.stringify(reviewedPredecessorEnvelope()) }]);
856
+ const client = nativeClient(queue.adapter);
857
+ const cli = {
858
+ ...closureCli({ risk: "medium", consumed: false }),
859
+ assess: client.assess.bind(client),
860
+ };
861
+ const details = await assessWith(cli, input) as AssessDetails & {
862
+ candidate: { consumed: boolean };
863
+ reviewDue: boolean;
864
+ reviewDueReason: string;
865
+ };
866
+ assert.equal(queue.calls.length, 1, "assessment must pass through the native CLI decoder");
867
+ assert.equal(details.risk, "medium", "supported predecessor evidence must not become unassessable");
868
+ assert.equal(details.candidate.consumed, false);
869
+ assert.equal(details.reviewDue, false);
870
+ assert.equal(details.reviewDueReason, "already_reviewed");
871
+ assert.equal(Object.hasOwn(details, "nextTransition"), false);
872
+ assert.equal(details.nativeReviewOutcome, "unknown", "already_reviewed is not exact-candidate consumption");
873
+ assert.equal(details.outcome_source, "unknown");
874
+ assert.deepEqual(details.plan, verificationPlan({
875
+ rddLine: RDD_LINE.ON,
876
+ risk: VERIFICATION_TIER.MEDIUM,
877
+ writerProfile: details.writerProfile as WriterProfile,
878
+ nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.UNKNOWN,
879
+ }));
880
+ assert.equal(details.plan.writerSelfVerification, true);
881
+ assert.equal(details.plan.structuralReadbackOnly, false);
882
+ }
883
+ });
884
+
809
885
  test("gentle_review assess: native consumed true derives closed for this candidate", async () => {
810
886
  const details = await assessWith(closureCli({ consumed: true }));
811
887
  assert.equal(details.nativeReviewOutcome, "closed");
812
888
  assert.equal(details.outcome_source, "derived");
813
889
  assert.equal(details.plan.writerSelfVerification, true);
814
- assert.equal(details.plan.independentVerifier, false, "a natively closed candidate restores the RDD on-path");
890
+ assert.equal(details.plan.independentVerifier, true, "a natively closed candidate keeps the risk-tier verifier (verify-always-rdd-high S1)");
815
891
  });
816
892
 
817
893
  test("gentle_review assess: a caller-declared closed without native consumed evidence fails closed to unknown", async () => {
@@ -827,7 +903,7 @@ test("gentle_review assess: a caller-declared closed corroborated by native cons
827
903
  const details = await assessWith(closureCli({ consumed: true }), { nativeReviewOutcome: "closed" });
828
904
  assert.equal(details.nativeReviewOutcome, "closed");
829
905
  assert.equal(details.outcome_source, "derived", "closure is attributed to the native evidence, not to the caller's claim");
830
- assert.equal(details.plan.independentVerifier, false);
906
+ assert.equal(details.plan.independentVerifier, true, "closure never removes the risk-tier verifier");
831
907
  });
832
908
 
833
909
  test("gentle_review assess: explicit declined, unavailable, or unknown beats native consumed (only ever raises the bar)", async () => {
@@ -1154,7 +1230,7 @@ test("native assess: a non-zero exit (an older binary reporting an unknown comma
1154
1230
  const textOnStdout = queuedAdapter([{ stdout: "unknown command \"assess\" for \"gentle-ai review\"", exitCode: 1 }]);
1155
1231
  await assert.rejects(
1156
1232
  () => nativeClient(textOnStdout.adapter).assess!({ cwd: process.cwd() }),
1157
- (error: unknown) => error instanceof NativeReviewCliError && [NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON, NATIVE_REVIEW_ERROR_CODE.NON_ZERO].includes(error.code),
1233
+ (error: unknown) => error instanceof NativeReviewCliError && (error.code === NATIVE_REVIEW_ERROR_CODE.MALFORMED_JSON || error.code === NATIVE_REVIEW_ERROR_CODE.NON_ZERO),
1158
1234
  );
1159
1235
  });
1160
1236
 
@@ -42,7 +42,7 @@ function nonPiV3Consent() {
42
42
  }
43
43
 
44
44
  function consentCustom(select: (title: string, options: string[]) => Promise<string | undefined>) {
45
- return async (factory: (...args: never[]) => unknown) => {
45
+ return async (factory: (...args: unknown[]) => unknown) => {
46
46
  let result: unknown;
47
47
  const component = factory(
48
48
  { terminal: { rows: 24 }, requestRender() {} },
@@ -241,15 +241,14 @@ function piConsent() {
241
241
  const decoded = consent();
242
242
  return {
243
243
  ...decoded,
244
- choices: decoded.choices.map((choice) => ({
245
- ...choice,
246
- invocation: choice.invocation.replace(" --consent ", " --agent pi --consent "),
247
- })) as typeof decoded.choices,
244
+ choices: [
245
+ { ...decoded.choices[0], invocation: decoded.choices[0].invocation.replace(" --consent ", " --agent pi --consent ") },
246
+ { ...decoded.choices[1], invocation: decoded.choices[1].invocation.replace(" --consent ", " --agent pi --consent ") },
247
+ ] as const,
248
248
  };
249
249
  }
250
250
 
251
251
  function controllerHarness(cwd: string, processEnv: NodeJS.ProcessEnv = {}, options: {
252
- now?: () => number;
253
252
  answerConsentError?: Error & { mutationOutcome?: "none" | "unknown" };
254
253
  startAction?: "created" | "resumed" | "replayed" | "closed" | "blocked-scope-action";
255
254
  childStandingReviewPermissionClient?: ChildStandingReviewPermissionClient;
@@ -284,7 +283,7 @@ function controllerHarness(cwd: string, processEnv: NodeJS.ProcessEnv = {}, opti
284
283
  return { kind: "started", start: { lineageId: `lineage-${answers.length}`, state: action === "blocked-scope-action" ? "unreviewed" : "approved", riskLevel: "high", selectedLenses: [], changedFiles: 1, changedLines: 2, correctionBudget: 0, action, lensesRequired: false, riskReasons: [] } };
285
284
  },
286
285
  } as unknown as NativeReviewCli;
287
- createGentleAiExtension({ nativeReviewCli: native, candidateViews: new CandidateViewRegistry(), processEnv, now: options.now, childStandingReviewPermissionClient: options.childStandingReviewPermissionClient })({
286
+ createGentleAiExtension({ nativeReviewCli: native, candidateViews: new CandidateViewRegistry(), processEnv, childStandingReviewPermissionClient: options.childStandingReviewPermissionClient })({
288
287
  on(name: string, handler: RegisteredEvent) { events.set(name, handler); },
289
288
  registerCommand(name: string, definition: RegisteredCommand) { commands.set(name, definition); },
290
289
  registerTool(definition: RegisteredTool & { name: string }) { tools.set(definition.name, definition); },
@@ -471,10 +470,11 @@ test("blocked-scope-action never persists host permission", async (t) => {
471
470
  assert.deepEqual(runtime.answers, ["granted", "declined"]);
472
471
  });
473
472
 
474
- test("a stale consent result with contextual native status never arms host permission", async (t) => {
473
+ test("a late consent answer is accepted once and arms host permission without another prompt", async (t) => {
475
474
  const cwd = reviewRepository(t);
476
475
  let now = 0;
477
- const runtime = controllerHarness(cwd, {}, { now: () => now });
476
+ t.mock.method(Date, "now", () => now);
477
+ const runtime = controllerHarness(cwd);
478
478
  const manager = {};
479
479
  let prompts = 0;
480
480
  const ctx = interactiveContext(cwd, manager, async (_title, options) => {
@@ -486,14 +486,13 @@ test("a stale consent result with contextual native status never arms host permi
486
486
  return options[1];
487
487
  });
488
488
  const start = { operation: "start", input: JSON.stringify({ mode: "ordinary" }) };
489
- const stale = (await runtime.controller.execute("expired-host-choice", start, undefined, undefined, ctx)).details;
490
- assert.equal(stale.outcome, "consent-binding-stale");
491
- assert.equal(stale.native_invocation_attempted, false);
492
- assert.equal(typeof stale.result, "object", "native STATUS is contextual evidence, not a successful START result");
489
+ const accepted = (await runtime.controller.execute("late-host-choice", start, undefined, undefined, ctx)).details;
490
+ assert.notEqual(accepted.outcome, "consent-binding-stale");
491
+ assert.deepEqual(runtime.answers, ["granted"]);
493
492
  writeFileSync(join(cwd, "app.ts"), "export const value = 3;\n");
494
- await runtime.controller.execute("after-stale", start, undefined, undefined, ctx);
495
- assert.equal(prompts, 2, "the next fresh consent envelope must still prompt after a stale contextual result");
496
- assert.deepEqual(runtime.answers, ["declined"]);
493
+ await runtime.controller.execute("after-late-answer", start, undefined, undefined, ctx);
494
+ assert.equal(prompts, 1, "a late session grant must not cause a second prompt");
495
+ assert.deepEqual(runtime.answers, ["granted", "granted"]);
497
496
  });
498
497
 
499
498
  test("a failed consent invocation never arms host permission", async (t) => {
@@ -624,7 +623,7 @@ test("a package-owned child replays its exact pending ordinary grant from a sibl
624
623
  const parentToChild = new PassThrough();
625
624
  const broker = new ParentStandingReviewPermissionBroker(
626
625
  { readable: childToParent, writable: parentToChild },
627
- (repositoryIdentity) => repositoryIdentity === parentIdentity.repositoryIdentity && parentManager.getSessionId() === "parent-session" && hasReviewSessionPermission(parentIdentity),
626
+ (repositoryIdentity) => repositoryIdentity === parentIdentity.repositoryIdentity && parentIdentity.sessionManager.getSessionId() === "parent-session" && hasReviewSessionPermission(parentIdentity),
628
627
  );
629
628
  const childPermission = new ChildStandingReviewPermissionClient(
630
629
  { readable: parentToChild, writable: childToParent },
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { spawn } from "node:child_process";
3
- import { PassThrough, Writable } from "node:stream";
3
+ import { Duplex, PassThrough, Writable } from "node:stream";
4
4
  import test from "node:test";
5
5
  import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
6
6
  import { AgentRunner, type ChildLike, type TaskRequest } from "../lib/agents-runner.ts";
@@ -111,6 +111,7 @@ async function productionChild(requests: number, authorize: () => boolean, optio
111
111
  stdio: options.withoutChannel ? ["ignore", "pipe", "pipe"] : ["ignore", "pipe", "pipe", options.stdioMode ?? productionFd3StdioMode],
112
112
  });
113
113
  const pipe = child.stdio[3];
114
+ assert.ok(pipe === null || pipe === undefined || pipe instanceof Duplex, "fd3 must be a bidirectional channel");
114
115
  const trace = options.diagnosticTrace;
115
116
  const onParentData = () => { if (trace !== undefined) trace.parentRawDataChunkCount += 1; };
116
117
  if (trace !== undefined && pipe !== null && pipe !== undefined) pipe.on("data", onParentData);
@@ -120,9 +121,9 @@ async function productionChild(requests: number, authorize: () => boolean, optio
120
121
  trace.parentAuthorizationCallbackCount += 1;
121
122
  return authorize();
122
123
  };
123
- const broker = pipe === null || pipe === undefined
124
- ? undefined
125
- : new ParentStandingReviewPermissionBroker({ readable: pipe, writable: pipe }, parentAuthorize, options);
124
+ const broker = pipe instanceof Duplex
125
+ ? new ParentStandingReviewPermissionBroker({ readable: pipe, writable: pipe }, parentAuthorize, options)
126
+ : undefined;
126
127
  if (options.closeChannel && pipe !== null && pipe !== undefined) {
127
128
  broker?.close();
128
129
  pipe.destroy();
@@ -323,7 +324,7 @@ test("fresh Jiti moduleCache:false reloads share fd3 structurally and reject sta
323
324
  stdio: ["ignore", "pipe", "pipe", productionFd3StdioMode],
324
325
  });
325
326
  const pipe = child.stdio[3];
326
- assert.ok(pipe);
327
+ assert.ok(pipe instanceof Duplex, "fd3 must be a bidirectional channel");
327
328
  const broker = new ParentStandingReviewPermissionBroker({ readable: pipe, writable: pipe }, () => true);
328
329
  let stdout = "";
329
330
  let stderr = "";
@@ -7,6 +7,7 @@ import {
7
7
  type ReviewMode,
8
8
  } from "../lib/review-snapshot.ts";
9
9
  import { existsSync, renameSync } from "node:fs";
10
+ import { correctionBudget, REVIEW_RISK_TIER } from "../lib/review-risk.ts";
10
11
  import type { ReviewLockPlatformAdapterV1 } from "../lib/review-lock.ts";
11
12
  import {
12
13
  REVIEW_EVENT,
@@ -45,6 +46,7 @@ export function testSnapshot(options: TestSnapshotOptions): SnapshotV1 {
45
46
  const lenses = options.lenses ?? [];
46
47
  const initialTree = options.initialTree ?? options.completeTree;
47
48
  const selected = lenses[0];
49
+ const changedLines = route === REVIEW_ROUTE.FULL_4R ? 401 : route === REVIEW_ROUTE.TRIVIAL ? 1 : 10;
48
50
  return {
49
51
  schema: "gentle-ai.review-snapshot/v1",
50
52
  mode: options.mode ?? REVIEW_MODE.ORDINARY,
@@ -57,9 +59,13 @@ export function testSnapshot(options: TestSnapshotOptions): SnapshotV1 {
57
59
  : { kind: REVIEW_PROJECTION.INTENDED_COMMIT, tree: initialTree },
58
60
  initial_review_tree: initialTree,
59
61
  genesis_paths: options.genesisPaths ?? ["app.ts", "src/auth.ts", "src/retry.ts", "src/review.ts"],
62
+ intended_untracked: [],
63
+ risk_tier: route === REVIEW_ROUTE.FULL_4R ? REVIEW_RISK_TIER.HIGH : route === REVIEW_ROUTE.TRIVIAL ? REVIEW_RISK_TIER.LOW : REVIEW_RISK_TIER.MEDIUM,
64
+ original_changed_lines: changedLines,
65
+ correction_budget: correctionBudget(changedLines),
60
66
  diff_evidence: {
61
67
  event: REVIEW_EVENT.ORDINARY_START,
62
- changedLines: route === REVIEW_ROUTE.FULL_4R ? 401 : route === REVIEW_ROUTE.TRIVIAL ? 1 : 10,
68
+ changedLines,
63
69
  triviality:
64
70
  route === REVIEW_ROUTE.TRIVIAL
65
71
  ? TRIVIALITY.PROVEN
@@ -33,6 +33,7 @@ import {
33
33
  createFrozenLedger,
34
34
  createReceiptEnvelope,
35
35
  createReviewState,
36
+ validateReviewGraphReplayV1,
36
37
  evaluateGateTarget,
37
38
  type CanonicalFrozenRowV1,
38
39
  type ReceiptBodyV1,
@@ -41,8 +42,12 @@ import {
41
42
  import { REVIEW_LENS, REVIEW_ROUTE } from "../lib/review-triggers.ts";
42
43
  import {
43
44
  ordinaryValidatorRequest,
45
+ recordOrdinaryDiscovery,
46
+ resolveOrdinaryEvidence,
47
+ recordOrdinaryFinalVerification,
44
48
  recordOrdinaryValidation,
45
49
  } from "../lib/review-policy-ordinary.ts";
50
+ import { createReviewEventV1 } from "../lib/review-graph-schema.ts";
46
51
  import { qualifiedReviewLockPlatform, testSnapshot } from "./review-test-fixtures.ts";
47
52
 
48
53
  const TREE = {
@@ -393,7 +398,19 @@ test("ordinary follow-ups are ID-sorted action-free validation evidence and do n
393
398
  assert.equal(recorded.phase, REVIEW_PHASE.FINAL_VERIFICATION);
394
399
  assert.equal(recorded.counters.validator_runs, fixed.counters.validator_runs + 1);
395
400
  assert.equal(recorded.current_candidate_tree, fixed.current_candidate_tree);
396
- assert.equal(recorded.follow_ups, undefined);
401
+ assert.equal(Object.hasOwn(recorded, "follow_ups"), false);
402
+ });
403
+
404
+ test("graph replay rejects a persisted non-boolean final verification instead of approving truthy input", () => {
405
+ const ready = resolveOrdinaryEvidence(recordOrdinaryDiscovery(state(), { rows: [] }), { deterministicResults: [] });
406
+ const approved = recordOrdinaryFinalVerification(ready, { passed: true });
407
+ const genesis = createReviewEventV1({ lineage_id: ready.lineage_id, sequence: 0, predecessor_event_id: null, kind: "lineage-created", reducer_transition: "start", reducer_input: ready, payload: { state: ready }, reduced_state_hash: canonicalHash(ready) });
408
+ for (const passed of ["false", "true", 0, 1, null]) {
409
+ const next = createReviewEventV1({ lineage_id: ready.lineage_id, sequence: 1, predecessor_event_id: genesis.event_id, kind: "operation-completed", reducer_transition: REVIEW_TRANSITION.ORDINARY_FINAL_VERIFICATION, reducer_input: { passed }, payload: { state: approved }, reduced_state_hash: canonicalHash(approved) });
410
+ assert.throws(() => validateReviewGraphReplayV1([genesis, next]), /reducer input.*passed/i);
411
+ }
412
+ const valid = createReviewEventV1({ lineage_id: ready.lineage_id, sequence: 1, predecessor_event_id: genesis.event_id, kind: "operation-completed", reducer_transition: REVIEW_TRANSITION.ORDINARY_FINAL_VERIFICATION, reducer_input: { passed: true }, payload: { state: approved }, reduced_state_hash: canonicalHash(approved) });
413
+ assert.deepEqual(validateReviewGraphReplayV1([genesis, valid]), approved);
397
414
  });
398
415
 
399
416
  test("new ordinary lineages fail closed when immutable genesis paths are absent", () => {
@@ -1,16 +1,14 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { spawnSync } from "node:child_process";
3
- import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import test from "node:test";
8
8
  import { DEFAULT_STAGES, runTestSuite } from "../scripts/run-test-suite.mjs";
9
9
 
10
- // `pnpm test` chains its three stages; #1285 requires that a stage-1 failure
11
- // never suppresses the later stages. These tests cover the runner's
12
- // orchestration contract with fake stage implementations, so no real test
13
- // process is spawned.
10
+ // #1285 requires that a stage-1 failure never suppresses later stages.
11
+ // Orchestration tests use fake stages; CLI tests exercise real child processes.
14
12
 
15
13
  function fakeRunStage(codes) {
16
14
  let index = 0;
@@ -113,6 +111,86 @@ test("direct invocation still runs stages when reached through a file symlink",
113
111
  assert.match(result.stdout ?? "", /all stages passed/);
114
112
  });
115
113
 
114
+ function probeStageEnvironment(t: test.TestContext, env: NodeJS.ProcessEnv) {
115
+ const stagesPath = writeStagesFile(t, []);
116
+ const probePath = join(dirname(stagesPath), "probe.mjs");
117
+ writeFileSync(probePath, `
118
+ import { existsSync, readdirSync } from "node:fs";
119
+ const home = process.env.GENTLE_PI_CONFIG_HOME;
120
+ console.log("ENV_PROBE=" + JSON.stringify({
121
+ git: Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith("GIT_"))),
122
+ agents: Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith("GENTLE_PI_AGENTS_"))),
123
+ home,
124
+ files: home && existsSync(home) ? readdirSync(home) : null,
125
+ sentinel: process.env.TEST_ENV_SENTINEL,
126
+ }));
127
+ `);
128
+ writeFileSync(stagesPath, JSON.stringify([
129
+ { name: "unit-tests", command: `node "${probePath}"` },
130
+ { name: "provider-contract", command: `node "${probePath}"` },
131
+ ]));
132
+ const result = spawnSync(process.execPath, [runnerPath, stagesPath], {
133
+ encoding: "utf8", env: { ...process.env, ...env, TEST_ENV_SENTINEL: "preserved" },
134
+ });
135
+ assert.equal(result.status, 0, result.stderr);
136
+ assert.equal(result.stderr, "");
137
+ assert.match(result.stdout, /PASS unit-tests/);
138
+ assert.match(result.stdout, /PASS provider-contract/);
139
+ const probes = result.stdout.split(/\r?\n/).filter((line) => line.startsWith("ENV_PROBE="))
140
+ .map((line) => JSON.parse(line.slice("ENV_PROBE=".length)));
141
+ assert.equal(probes.length, 2);
142
+ for (const probe of probes) assert.equal(probe.sentinel, "preserved");
143
+ return probes;
144
+ }
145
+
146
+ test("unit stage strips inherited Git configuration without changing later stages", (t) => {
147
+ const env = {
148
+ GIT_CONFIG_COUNT: "2",
149
+ GIT_CONFIG_KEY_0: "credential.interactive", GIT_CONFIG_VALUE_0: "false",
150
+ GIT_CONFIG_KEY_1: "credential.guiPrompt", GIT_CONFIG_VALUE_1: "false",
151
+ GIT_DIR: "inherited-repository", GIT_AUTHOR_NAME: "inherited-author",
152
+ };
153
+ const [unit, provider] = probeStageEnvironment(t, env);
154
+ assert.deepEqual(unit.git, {});
155
+ for (const [key, value] of Object.entries(env)) assert.equal(provider.git[key], value);
156
+ });
157
+
158
+ test("unit stage strips inherited subagent context without changing later stages", (t) => {
159
+ const env = { GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: "1" };
160
+ const [unit, provider] = probeStageEnvironment(t, env);
161
+ assert.deepEqual(unit.agents, {});
162
+ for (const [key, value] of Object.entries(env)) assert.equal(provider.agents[key], value);
163
+ });
164
+
165
+ test("unit stage accepts an uncreated inherited config home without creating it", (t) => {
166
+ const dir = mkdtempSync(join(tmpdir(), "gentle-pi-caller-config-"));
167
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
168
+ const home = join(dir, "not-created");
169
+ assert.equal(existsSync(home), false);
170
+ const [unit, provider] = probeStageEnvironment(t, { GENTLE_PI_CONFIG_HOME: home });
171
+ assert.notEqual(unit.home, home);
172
+ assert.deepEqual(unit.files, []);
173
+ assert.equal(existsSync(unit.home), false);
174
+ assert.equal(provider.home, home);
175
+ assert.equal(provider.files, null);
176
+ assert.equal(existsSync(home), false);
177
+ });
178
+
179
+ test("unit stage uses an empty temporary config home and removes it after exit", (t) => {
180
+ const home = mkdtempSync(join(tmpdir(), "gentle-pi-caller-config-"));
181
+ t.after(() => rmSync(home, { recursive: true, force: true }));
182
+ const configPath = join(home, "runtime-guardrails.json");
183
+ const config = JSON.stringify({ autonomousMode: true, guardedCommands: { gitPush: "allow", gitRebase: "allow", gitBranchDeleteForce: "allow" } });
184
+ writeFileSync(configPath, config);
185
+ const [unit, provider] = probeStageEnvironment(t, { GENTLE_PI_CONFIG_HOME: home });
186
+ assert.notEqual(unit.home, home);
187
+ assert.deepEqual(unit.files, []);
188
+ assert.equal(existsSync(unit.home), false);
189
+ assert.equal(provider.home, home);
190
+ assert.deepEqual(provider.files, ["runtime-guardrails.json"]);
191
+ assert.equal(readFileSync(configPath, "utf8"), config);
192
+ });
193
+
116
194
  test("direct invocation rejects a malformed stages file with a non-zero exit", (t) => {
117
195
  const dir = mkdtempSync(join(tmpdir(), "gentle-pi-run-test-suite-"));
118
196
  t.after(() => rmSync(dir, { recursive: true, force: true }));
@@ -390,7 +390,24 @@ async function run() {
390
390
  assert.ok(flags.has("no-skill-registry"), "missing no-skill-registry flag");
391
391
  assert.ok(hooks.has("session_start"), "missing session_start hook");
392
392
  assert.ok(hooks.has("session_shutdown"), "missing session_shutdown hook");
393
- assert.equal(hooks.has("input"), false, "retired SDD slash input must not be intercepted");
393
+ assert.ok(hooks.has("input"), "missing live input provenance hook");
394
+ for (const text of [
395
+ "/sdd-init", "/sdd-continue", "/sdd-status", "/gentle-sdd-init",
396
+ "/gentle-sdd-continue", "/gentle-sdd-status", "/gentle:install-sdd",
397
+ "/gentle:sdd-preflight", "/gentle-ai:install-sdd", "/gentle-ai:sdd-preflight",
398
+ "/gentle-ai:sdd-status", "/gentle-ai:sdd-continue",
399
+ "Inspect this project without changing files.", "/ordinary-unregistered-command",
400
+ ]) {
401
+ const event = { text, source: "interactive", images: [] };
402
+ const before = structuredClone(event);
403
+ const emittedBefore = emittedEvents.length;
404
+ for (const handler of hooks.get("input")) {
405
+ assert.deepEqual(await handler(event, createCtx(ROOT)), { action: "continue" },
406
+ `input must pass through without slash interception: ${text}`);
407
+ }
408
+ assert.deepEqual(event, before, "input text and attachments must remain unchanged");
409
+ assert.equal(emittedEvents.length, emittedBefore, "input must not dispatch slash logic");
410
+ }
394
411
  assert.ok(hooks.has("before_agent_start"), "missing before_agent_start hook");
395
412
  assert.ok(hooks.has("tool_call"), "missing tool_call hook");
396
413
  for (const toolName of ["read", "grep", "find", "ls", "edit", "write"]) {
@@ -450,7 +467,8 @@ async function run() {
450
467
  // rules) moved verbatim to assets/orchestrator-delegation.md; the
451
468
  // always-on combined prompt now only carries a pointer to it. Union read
452
469
  // so these assertions are repointed, not weakened.
453
- const delegationDetail = await readFile(join(ROOT, "assets", "orchestrator-delegation.md"), "utf8");
470
+ const { DELEGATION_MODULES } = await import("./support/orchestrator-modules.ts");
471
+ const delegationDetail = (await Promise.all(DELEGATION_MODULES.map((file) => readFile(join(ROOT, "assets", file), "utf8")))).join("\n\n");
454
472
 
455
473
  const promptCwd = await tempWorkspace();
456
474
  try {
@@ -1018,8 +1036,8 @@ async function run() {
1018
1036
  assert.equal(existsSync(installedExplorePath), true);
1019
1037
  assert.deepEqual(
1020
1038
  readAgentDefinition(await readFile(installedExplorePath, "utf8")),
1021
- { name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph"] },
1022
- "isolated package installation must activate only the explorer inspection tools",
1039
+ { name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph", "web_enable", "web_search", "source_check", "fetch_content", "get_search_content"] },
1040
+ "isolated package installation must declare the explorer's local and direct web scope",
1023
1041
  );
1024
1042
  const installedRiskSource = await readFile(
1025
1043
  join(globalAgentHome, "agents", "review-risk.md"),
@@ -1316,15 +1334,15 @@ async function run() {
1316
1334
  );
1317
1335
  await writeFile(globalModelsPath, JSON.stringify({ "sdd-apply": {} }, null, 2));
1318
1336
  await hooks.get("session_start")[0]({ reason: "startup" }, legacyCtx);
1319
- const explicitInheritClearsAgent = await readFile(
1337
+ const savedInheritPreservesAgent = await readFile(
1320
1338
  join(legacyModelsCwd, ".pi", "agents", "sdd-apply.md"),
1321
1339
  "utf8",
1322
1340
  );
1323
- assert.doesNotMatch(explicitInheritClearsAgent, /model:/);
1324
- const explicitInheritClearsProfiles = JSON.parse(
1341
+ assert.match(savedInheritPreservesAgent, /model: global\/provider-model/);
1342
+ const savedInheritPreservesProfiles = JSON.parse(
1325
1343
  await readFile(join(legacyModelsCwd, ".pi", "subagents.json"), "utf8"),
1326
1344
  );
1327
- assert.equal(explicitInheritClearsProfiles.model_profiles, undefined);
1345
+ assert.equal(savedInheritPreservesProfiles.model_profiles["sdd-apply"].model, "global/provider-model");
1328
1346
  } finally {
1329
1347
  await rm(legacyModelsCwd, { recursive: true, force: true });
1330
1348
  await rm(globalModelsPath, { force: true });
@@ -1346,10 +1364,12 @@ async function run() {
1346
1364
  );
1347
1365
  await writeFile(globalModelsPath, JSON.stringify({ worker: {} }, null, 2));
1348
1366
  await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(staleSettingsOnlyCwd, true));
1349
- const staleOnlyClearedProfiles = JSON.parse(
1367
+ const staleOnlyPreservedProfiles = JSON.parse(
1350
1368
  await readFile(globalSubagentsPath, "utf8"),
1351
1369
  );
1352
- assert.equal(staleOnlyClearedProfiles.model_profiles, undefined);
1370
+ assert.deepEqual(staleOnlyPreservedProfiles.model_profiles, {
1371
+ worker: { model: "stale/model", effort: "high" },
1372
+ });
1353
1373
  } finally {
1354
1374
  await rm(staleSettingsOnlyCwd, { recursive: true, force: true });
1355
1375
  await rm(globalModelsPath, { force: true });
@@ -1565,22 +1585,29 @@ async function run() {
1565
1585
  assert.match(preservedProjectSubagentWorker, /thinking: medium/);
1566
1586
  await writeFile(globalModelsPath, JSON.stringify({ worker: {} }, null, 2));
1567
1587
  await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(modelsCwd, true));
1568
- const clearedProfiles = JSON.parse(
1588
+ // gentle-ai#4946: startup/reload never treats persisted inherit as
1589
+ // deletion approval, including for previously materialized routing.
1590
+ // Explicit panel saves and confirmed profile replacement still clear.
1591
+ const preservedClearProfiles = JSON.parse(
1569
1592
  await readFile(join(modelsCwd, ".pi", "subagents.json"), "utf8"),
1570
1593
  );
1571
- assert.equal(clearedProfiles.model_profiles, undefined);
1594
+ assert.equal(
1595
+ preservedClearProfiles.model_profiles.worker.model,
1596
+ "existing/model",
1597
+ );
1598
+ assert.equal(preservedClearProfiles.model_profiles.worker.effort, "high");
1572
1599
  const unchangedProjectWorker = await readFile(
1573
1600
  join(modelsCwd, ".pi", "agents", "worker.md"),
1574
1601
  "utf8",
1575
1602
  );
1576
1603
  assert.match(unchangedProjectWorker, /model: existing\/project-worker/);
1577
1604
  assert.match(unchangedProjectWorker, /thinking: high/);
1578
- const clearedProjectSubagentWorker = await readFile(
1605
+ const preservedClearProjectSubagentWorker = await readFile(
1579
1606
  join(modelsCwd, ".pi", "subagents", "worker.md"),
1580
1607
  "utf8",
1581
1608
  );
1582
- assert.doesNotMatch(clearedProjectSubagentWorker, /model:/);
1583
- assert.doesNotMatch(clearedProjectSubagentWorker, /thinking:/);
1609
+ assert.match(preservedClearProjectSubagentWorker, /model: existing\/project-subagent-worker/);
1610
+ assert.match(preservedClearProjectSubagentWorker, /thinking: medium/);
1584
1611
 
1585
1612
  await writeFile(
1586
1613
  globalModelsPath,