gentle-pi 3.7.0 → 4.1.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 (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  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 +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -9,9 +9,9 @@ import { dirname, join, resolve, sep } from "node:path";
9
9
  import test from "node:test";
10
10
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
11
  import { __testing, createGentleAiExtension, PendingReviewConsentRegistry } from "../extensions/gentle-ai.ts";
12
- import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
12
+ import { CandidateViewError, CandidateViewRegistry } from "../lib/review-candidate-view.ts";
13
13
  import { NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError, NativeReviewConsentRequiredError, type NativeReviewCli } from "../lib/native-review-cli.ts";
14
- import { decodeReviewConsentV3, decodeReviewStatusV3, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
14
+ import { decodeReviewLastEventClosureV1, decodeReviewConsentV3, decodeReviewStatusV3, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
15
15
 
16
16
  const SHA = `sha256:${"a".repeat(64)}`;
17
17
  const TREE = "b".repeat(40);
@@ -129,6 +129,149 @@ test("STATUS renders the managed_assets_outdated continuation command as the act
129
129
  assert.equal(degraded.hint, undefined);
130
130
  });
131
131
 
132
+ test("caller-relative STATUS approved guidance preserves selected root against a different session", async () => {
133
+ const lineageId = "caller-relative-status";
134
+ const root = realpathSync(process.cwd());
135
+ const native = { targetStatus: async () => approvedAcknowledgementStatus(lineageId, root) } as unknown as NativeReviewCli;
136
+ const result = await __testing.executeReviewControllerOperation({ operation: "status", lineageId, workspaceRoot: root }, dirname(root), native);
137
+ assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(root)}}`);
138
+ });
139
+
140
+ test("caller-relative approved closure guidance preserves selected root and canonical omission", async () => {
141
+ const lineageId = "caller-relative-closure";
142
+ const root = realpathSync(process.cwd());
143
+ const { submission: _submission, artifactSubject: _artifactSubject, ...base } = collectInput(lineageId);
144
+ const input = { ...base, name: "provider_refuter", schema: "https://gentle-ai.dev/schema/review/refuter/v1", captureOperation: "review.capture-refuter", arguments: [
145
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
146
+ { name: "target", value: SHA, token: `--target=${SHA}` },
147
+ { name: "agent", value: "pi", token: "--agent=pi" },
148
+ { name: "execute", value: "true", token: "--execute=true" },
149
+ ] } as unknown as ReviewCollectInputV3;
150
+ const transition = approvedAcknowledgementStatus(lineageId, root).nextTransition;
151
+ assert.equal(transition?.kind, "execute");
152
+ if (transition?.kind !== "execute") throw new Error("expected acknowledgement transition");
153
+ const acknowledgement = transition.execute;
154
+ const native = {
155
+ targetStatus: async () => ({ ...status(lineageId), nextTransition: { kind: "collect", reasonCode: "provider_role_required", collect: { inputs: [input] } } }),
156
+ captureProviderRole: async () => ({ schema: "gentle-ai.review-last-event-closure/v1", operation: "review.capture-refuter", lineageId, state: "approved", storeRevision: SHA, acknowledgement }),
157
+ } as unknown as NativeReviewCli;
158
+ const result = await __testing.executeReviewCaptureOperation({ lineageId, workspaceRoot: root, collectBinding: JSON.stringify(input) }, dirname(root), native);
159
+ assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(root)}}`);
160
+ const canonical = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input) }, root, native);
161
+ assert.equal(canonical.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}"}`);
162
+ assert.deepEqual((result.closure as Record<string, unknown>).acknowledgement, (canonical.closure as Record<string, unknown>).acknowledgement);
163
+ // This root-only registry double models a remembered lineage without a
164
+ // frozen projection, and enforces the same root identity at admission.
165
+ const boundViews = {
166
+ resolveWorkspaceRoot: (requestedLineage: string) => {
167
+ assert.equal(requestedLineage, lineageId);
168
+ return root;
169
+ },
170
+ assertWorkspaceRoot: (requestedLineage: string, requestedRoot: string) => {
171
+ assert.equal(requestedLineage, lineageId);
172
+ assert.equal(realpathSync(requestedRoot), root);
173
+ },
174
+ hasProjection: () => false,
175
+ } as unknown as CandidateViewRegistry;
176
+ const bound = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input) }, dirname(root), native, undefined, boundViews);
177
+ assert.equal(bound.next_action, canonical.next_action);
178
+ });
179
+
180
+ test("caller-relative grouped capture closure preserves selected workspace root", async (t) => {
181
+ t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
182
+ const lineageId = "caller-relative-grouped";
183
+ const root = realpathSync(process.cwd());
184
+ const inputs = (["review-risk", "review-resilience", "review-readability", "review-reliability"] as const).map((lens, order) => {
185
+ const input = collectInput(lineageId);
186
+ input.arguments = input.arguments.map((argument) => {
187
+ const value = argument.name === "lens" ? lens : argument.name === "order" ? String(order) : argument.name === "subject-hash" ? `sha256:${String(order).repeat(64)}` : argument.value;
188
+ return { ...argument, value, token: `--${argument.name}=${value}` };
189
+ });
190
+ input.submission!.argumentTokens = [...input.arguments.map((argument) => argument.token!), "--input={{value}}"];
191
+ input.arguments = [...input.arguments, { name: "agent", value: "pi", token: "--agent=pi" }, { name: "materialize", value: "true", token: "--materialize=true" }];
192
+ input.artifactSubject = { ...input.artifactSubject!, lens, selectedOrder: order, subjectHash: `sha256:${String(order).repeat(64)}` };
193
+ return input;
194
+ });
195
+ const transition = approvedAcknowledgementStatus(lineageId, root).nextTransition;
196
+ if (transition?.kind !== "execute") throw new Error("expected acknowledgement transition");
197
+ const acknowledgement = transition.execute;
198
+ const native = { targetStatus: async (request: { cwd: string }) => {
199
+ assert.equal(request.cwd, root);
200
+ return { ...status(lineageId, inputs), repositoryContext: { capability: "review.opaque_repository_context", handle: `rctx1_${"c".repeat(64)}`, revision: SHA, targetIdentity: SHA } };
201
+ } } as unknown as NativeReviewCli;
202
+ const wireAcknowledgement = { ...acknowledgement, binding: { lineage_id: lineageId, revision: SHA, target_identity: SHA } };
203
+ const wireClosure = { schema: "gentle-ai.review-last-event-closure/v1", operation: "review/capture-result", lineage_id: lineageId, state: "approved", store_revision: SHA, action: "approved by reviewers", acknowledgement: wireAcknowledgement };
204
+ const decodedClosure = decodeReviewLastEventClosureV1(wireClosure);
205
+ assert.equal(decodedClosure.lineageId, lineageId);
206
+ assert.deepEqual(decodedClosure.acknowledgement?.raw, wireAcknowledgement);
207
+ let submissions = 0;
208
+ __testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => requests.map((request) => ({ request, promptByteLength: 64, resultByteLength: 32 })), async () => {
209
+ submissions += 1;
210
+ return { promptByteLength: 64, resultByteLength: 32, submission: JSON.stringify(wireClosure) };
211
+ });
212
+ const result = await __testing.executeReviewCaptureGroupOperation({ lineageId, workspaceRoot: root, collectBindings: inputs.map((input) => JSON.stringify(input)), reviewerRunAcknowledged: true }, dirname(root), native);
213
+ assert.equal(submissions, 1, JSON.stringify(result));
214
+ assert.equal(result.outcome, "native-last-event-closure", JSON.stringify(result));
215
+ assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(root)}}`);
216
+ });
217
+
218
+ // Issue #1371: approved status mappings and closure envelopes provide additive next_action
219
+ test("STATUS on approved target renders additive next_action naming exact gentle_review acknowledge-approved invocation", async () => {
220
+ const lineageId = "status-approved-ack";
221
+ const native = { targetStatus: async () => approvedAcknowledgementStatus(lineageId) } as unknown as NativeReviewCli;
222
+ const result = await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, process.cwd(), native);
223
+ assert.equal(result.status, "blocked");
224
+ assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}"}`);
225
+ });
226
+
227
+ test("STATUS on approved target preserves workspaceRoot in next_action when distinct from process cwd", async (t) => {
228
+ const lineageId = "status-approved-ack-worktree";
229
+ const worktreeRoot = repository(t);
230
+ const native = { targetStatus: async () => approvedAcknowledgementStatus(lineageId, worktreeRoot) } as unknown as NativeReviewCli;
231
+ const result = await __testing.executeReviewControllerOperation({ operation: "status", lineageId, workspaceRoot: worktreeRoot }, worktreeRoot, native);
232
+ assert.equal(result.status, "blocked");
233
+ assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(worktreeRoot)}}`);
234
+ });
235
+
236
+ test("invalid acknowledgement input returns self-healing next_action for canonical lineage and fallback slug otherwise", async (t) => {
237
+ const lineageId = "canonical-lineage-ack";
238
+ const native = { targetStatus: async () => approvedAcknowledgementStatus(lineageId) } as unknown as NativeReviewCli;
239
+
240
+ // 1. Controller-only input with canonical lineage (no workspaceRoot)
241
+ const rejectedWithInput = await __testing.executeReviewControllerOperation(
242
+ { operation: "acknowledge-approved", lineageId, input: "{}" },
243
+ process.cwd(),
244
+ native,
245
+ );
246
+ assert.equal(rejectedWithInput.outcome, "native-approved-acknowledgement-input-invalid");
247
+ assert.equal(rejectedWithInput.reason, "controller-only-input");
248
+ assert.equal(rejectedWithInput.field, "input");
249
+ assert.equal(rejectedWithInput.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}"}`);
250
+
251
+ // 2. Controller-only input with canonical lineage and distinct workspaceRoot
252
+ const worktreeRoot = repository(t);
253
+ const rejectedWithWorktree = await __testing.executeReviewControllerOperation(
254
+ { operation: "acknowledge-approved", lineageId, input: "{}", workspaceRoot: worktreeRoot },
255
+ process.cwd(),
256
+ native,
257
+ );
258
+ assert.equal(rejectedWithWorktree.outcome, "native-approved-acknowledgement-input-invalid");
259
+ assert.equal(
260
+ rejectedWithWorktree.next_action,
261
+ `gentle_review {"operation":"acknowledge-approved","lineageId":"${lineageId}","workspaceRoot":${JSON.stringify(worktreeRoot)}}`,
262
+ );
263
+
264
+ // 3. Non-canonical / invalid lineage returns fallback slug
265
+ const rejectedNonCanonical = await __testing.executeReviewControllerOperation(
266
+ { operation: "acknowledge-approved", lineageId: "invalid lineage with newline\n", input: "{}" },
267
+ process.cwd(),
268
+ native,
269
+ );
270
+ assert.equal(rejectedNonCanonical.outcome, "native-approved-acknowledgement-input-invalid");
271
+ assert.equal(rejectedNonCanonical.reason, "controller-only-input");
272
+ assert.equal(rejectedNonCanonical.next_action, "resubmit-the-exact-lineage-without-controller-only-input");
273
+ });
274
+
132
275
  test("public acknowledgement relays one current provider vector and never replays after authority burn", async () => {
133
276
  const lineageId = "acknowledge-approved";
134
277
  const requests: Array<Record<string, unknown>> = [];
@@ -463,7 +606,10 @@ test("interleaved sessions sharing a CLI retain only their own capture routes",
463
606
  const index = requests.length - 1;
464
607
  if (index < 2) { [readyA, readyB][index]!(); await waits[index]!; }
465
608
  const lineageId = String(request.lineageId);
466
- return status(lineageId, [inputs.get(lineageId)!]);
609
+ // Cache absence alone is not foreign-session evidence. The provider
610
+ // rejects selectorless requests for these committed-range candidates.
611
+ return request.baseRef === (lineageId === a ? "base-a" : "base-b")
612
+ ? status(lineageId, [inputs.get(lineageId)!]) : status(lineageId, []);
467
613
  },
468
614
  captureCorrectionPlan: async ({ argumentTokens }: { argumentTokens: readonly string[] }) => {
469
615
  captures += 1;
@@ -487,7 +633,25 @@ test("interleaved sessions sharing a CLI retain only their own capture routes",
487
633
  outcomes: [ownA, ownB, foreign, cleaned].map(({ details }) => (details as { outcome?: string }).outcome),
488
634
  revalidationCalls: requests.length - 2,
489
635
  captures,
490
- }, { outcomes: ["native-last-event-closure", "native-last-event-closure", "capture-binding-rejected", "capture-binding-rejected"], revalidationCalls: 2, captures: 2 });
636
+ }, { outcomes: ["native-last-event-closure", "native-last-event-closure", "capture-binding-rejected", "capture-binding-rejected"], revalidationCalls: 4, captures: 2 });
637
+ });
638
+
639
+ test("capture route recovery uses a trusted committed projection through unknown-outcome reconciliation", async (t) => {
640
+ const candidateViews = new CandidateViewRegistry();
641
+ t.after(() => candidateViews.cleanupAll());
642
+ const cwd = repository(t), lineageId = "recovered-committed", input = correctionPlanInput(lineageId);
643
+ const view = candidateViews.create({ contributorRoot: cwd, baseRef: execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim(), committedOnly: true });
644
+ candidateViews.retain(view.token, lineageId);
645
+ const target = candidateViews.resolveProjection(lineageId, cwd), requests: Array<Record<string, unknown>> = [];
646
+ const native = {
647
+ targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return status(lineageId, [input]); },
648
+ captureCorrectionPlan: async () => { throw Object.assign(new Error("lost response"), { mutationOutcome: "unknown", nextAction: "review.status" }); },
649
+ } as unknown as NativeReviewCli;
650
+ const result = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input), correctionLines: 1 }, cwd, native, undefined, candidateViews, new Map(), true);
651
+ assert.equal(result.outcome, "native-capture-outcome-unknown");
652
+ assert.equal(requests.length, 2, "one admission STATUS and one reconciliation, never capture replay");
653
+ assert.ok(requests.every((request) => request.cwd === cwd && request.lineageId === lineageId && request.baseRef === target.baseCommit && request.committedOnly === true));
654
+ assert.equal(requests[0]!.agent, "pi");
491
655
  });
492
656
 
493
657
  test("REPAIR retains frozen committed collect selectors and leaves workspace routes unselected", async (t) => {
@@ -649,7 +813,8 @@ test("STATUS preserves retained intended-untracked selection through selectorles
649
813
  selections,
650
814
  true,
651
815
  );
652
- assert.deepEqual({ outcome: stale.outcome, requests: requests.length, captures }, { outcome: "capture-binding-rejected", requests: 3, captures: 0 });
816
+ assert.deepEqual({ outcome: stale.outcome, requests: requests.length, captures }, { outcome: "capture-binding-rejected", requests: 4, captures: 0 });
817
+ assert.deepEqual(requests[3], { cwd, lineageId, agent: "pi", ...selectedUntracked });
653
818
 
654
819
  const captured = await __testing.executeReviewCaptureOperation(
655
820
  { lineageId, collectBinding: bindingB, correctionLines: 1 },
@@ -666,6 +831,7 @@ test("STATUS preserves retained intended-untracked selection through selectorles
666
831
  { cwd, lineageId, agent: "pi", baseRef: "main", committedOnly: true },
667
832
  { cwd, lineageId, agent: "pi", ...selectedUntracked },
668
833
  { cwd, lineageId, agent: "pi", ...selectedUntracked },
834
+ { cwd, lineageId, agent: "pi", ...selectedUntracked },
669
835
  ]);
670
836
  assert.equal(captures, 1);
671
837
 
@@ -676,10 +842,14 @@ test("STATUS preserves retained intended-untracked selection through selectorles
676
842
  });
677
843
 
678
844
  test("route retention caps, rejects collisions and invalid selectors, and clears every terminal state", async () => {
679
- const native = { targetStatus: async (request: Record<string, unknown>) => status(String(request.lineageId)) } as unknown as NativeReviewCli;
845
+ const requests: Array<Record<string, unknown>> = [];
846
+ const native = { targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return status(String(request.lineageId), request.baseRef === undefined ? [] : [collectInput(String(request.lineageId))]); } } as unknown as NativeReviewCli;
680
847
  const selections = new Map();
681
848
  for (let index = 0; index <= 64; index += 1) await __testing.executeReviewControllerOperation({ operation: "status", lineageId: `bounded-${index}`, input: JSON.stringify({ baseRef: `base-${index}`, committedOnly: true }) }, process.cwd(), native, undefined, undefined, undefined, selections);
849
+ assert.equal(selections.size, 64, "route retention remains bounded before native refresh");
682
850
  const evicted = await __testing.executeReviewCaptureOperation({ lineageId: "bounded-0", collectBinding: JSON.stringify(collectInput("bounded-0")) }, process.cwd(), native, undefined, undefined, selections, true);
851
+ assert.equal(requests.length, 66, "eviction must negotiate fresh native STATUS before rejection");
852
+ assert.deepEqual(requests.at(-1), { cwd: process.cwd(), lineageId: "bounded-0", agent: "pi" });
683
853
  const collision = new Map(), lineageId = "route-collision", input = correctionPlanInput(lineageId);
684
854
  await __testing.executeReviewControllerOperation({ operation: "status", lineageId, input: JSON.stringify({ baseRef: "base-a", committedOnly: true }) }, process.cwd(), { targetStatus: async () => status(lineageId, [input]) } as unknown as NativeReviewCli, undefined, undefined, undefined, collision);
685
855
  const rejected = await __testing.executeReviewControllerOperation({ operation: "status", lineageId, input: JSON.stringify({ baseRef: "base-b", committedOnly: true }) }, process.cwd(), { targetStatus: async () => status(lineageId, [input]) } as unknown as NativeReviewCli, undefined, undefined, undefined, collision);
@@ -936,6 +1106,72 @@ function startStatus(cwd: string, baseRef?: string, intendedUntracked: readonly
936
1106
  }
937
1107
  }
938
1108
 
1109
+ test("START reports POSIX candidate parent privacy refusal without native START or permission repair", { skip: process.platform === "win32" }, async (t) => {
1110
+ const cwd = repository(t);
1111
+ const target = startStatus(cwd);
1112
+ const parent = join(cwd, ".git", "gentle-ai", "candidate-views");
1113
+ chmodSync(parent, 0o777);
1114
+ let starts = 0;
1115
+ const native = {
1116
+ targetStatus: async () => target,
1117
+ start: async () => { starts++; throw new Error("native START must not run"); },
1118
+ } as unknown as NativeReviewCli;
1119
+ let result: Record<string, unknown>;
1120
+ try {
1121
+ result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native);
1122
+ } finally {
1123
+ assert.equal(fs.lstatSync(parent).mode & 0o777, 0o777);
1124
+ chmodSync(parent, 0o700);
1125
+ }
1126
+ assert.equal(result.status, "blocked");
1127
+ assert.equal(result.outcome, "native-operation-failed");
1128
+ assert.equal(result.lineage_created, false);
1129
+ assert.equal(result.mutation_outcome, "none");
1130
+ assert.deepEqual(result.diagnostics, {
1131
+ code: "candidate-owner-parent-privacy",
1132
+ message: "candidate-views parent must be owned by the current user and inaccessible to group and others; inspect its ownership and permissions, then correct them out of band before retrying START",
1133
+ });
1134
+ assert.equal(starts, 0);
1135
+ });
1136
+
1137
+ test("START reports ineffective POSIX private-mode mounts without native START or permission repair", { skip: process.platform === "win32" }, async (t) => {
1138
+ const cwd = repository(t);
1139
+ const target = startStatus(cwd);
1140
+ const parent = join(cwd, ".git", "gentle-ai", "candidate-views");
1141
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
1142
+ chmodSync(parent, 0o777);
1143
+ const originalLstat = fs.lstatSync;
1144
+ t.mock.method(fs, "lstatSync", (path: Parameters<typeof fs.lstatSync>[0], ...args: unknown[]) => {
1145
+ const stat = (originalLstat as (...arguments_: unknown[]) => ReturnType<typeof fs.lstatSync>)(path, ...args);
1146
+ return typeof path === "string" && path.includes(".gentle-ai-chmod-probe-")
1147
+ ? Object.assign(Object.create(Object.getPrototypeOf(stat)), stat, { mode: (Number(stat.mode) & ~0o777) | 0o777, uid: process.getuid?.() })
1148
+ : stat;
1149
+ });
1150
+ syncBuiltinESMExports();
1151
+ t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
1152
+ let starts = 0;
1153
+ const native = {
1154
+ targetStatus: async () => target,
1155
+ start: async () => { starts++; throw new Error("native START must not run"); },
1156
+ } as unknown as NativeReviewCli;
1157
+ let result: Record<string, unknown>;
1158
+ try {
1159
+ result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native);
1160
+ } finally {
1161
+ assert.equal(fs.lstatSync(parent).mode & 0o777, 0o777);
1162
+ chmodSync(parent, 0o700);
1163
+ }
1164
+ assert.equal(result.status, "blocked");
1165
+ assert.equal(result.outcome, "native-operation-failed");
1166
+ assert.equal(result.lineage_created, false);
1167
+ assert.equal(result.mutation_outcome, "none");
1168
+ assert.deepEqual(result.diagnostics, {
1169
+ code: "candidate-owner-parent-chmod-ineffective",
1170
+ message: "candidate-views parent filesystem does not honor POSIX permission changes; move this repository's Git common directory to a POSIX-metadata filesystem or enable metadata support before retrying START",
1171
+ });
1172
+ assert.equal(starts, 0);
1173
+ });
1174
+
939
1175
  test("ordinary START binds the native workspace candidate and returns the native result", async (t) => {
940
1176
  const cwd = repository(t);
941
1177
  const target = startStatus(cwd);
@@ -1045,6 +1281,171 @@ test("ordinary START adopts the offered committed-range base while an untracked
1045
1281
  assert.equal(startCalls, 1, "native START is reached exactly once without drift");
1046
1282
  });
1047
1283
 
1284
+ test("ordinary START adopts a provider-offered base tree without treating it as caller input", async (t) => {
1285
+ const cwd = repository(t);
1286
+ execFileSync("git", ["add", "tracked.txt"], { cwd });
1287
+ execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "candidate"], { cwd });
1288
+ const baseCommit = execFileSync("git", ["rev-parse", "HEAD~1"], { cwd, encoding: "utf8" }).trim();
1289
+ const baseTree = execFileSync("git", ["rev-parse", `${baseCommit}^{tree}`], { cwd, encoding: "utf8" }).trim();
1290
+ const offered = startStatus(cwd);
1291
+ offered.nextTransition = {
1292
+ kind: "execute", reasonCode: "start_required",
1293
+ execute: { operation: "review.start", arguments: [
1294
+ { name: "target", value: offered.targetIdentity, token: `--target=${offered.targetIdentity}` },
1295
+ { name: "projection", value: "workspace", token: "--projection=workspace" },
1296
+ { name: "base-ref", value: baseTree, token: `--base-ref=${baseTree}` },
1297
+ { name: "committed-only", value: "true", token: "--committed-only=true" },
1298
+ { name: "agent", value: "pi", token: "--agent=pi" },
1299
+ ], preconditions: [], binding: {} },
1300
+ } as unknown as ReviewStatusV3["nextTransition"];
1301
+ const adopted = startStatus(cwd, baseCommit);
1302
+ const requests: Array<Record<string, unknown>> = [];
1303
+ const starts: Array<Record<string, unknown>> = [];
1304
+ const native = {
1305
+ targetStatus: async (request: Record<string, unknown>) => {
1306
+ requests.push(request);
1307
+ return requests.length === 1 ? offered : adopted;
1308
+ },
1309
+ start: async (request: Record<string, unknown>) => {
1310
+ starts.push(request);
1311
+ return { lineageId: "tree-offer-started", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [], raw: {} };
1312
+ },
1313
+ } as unknown as NativeReviewCli;
1314
+ const views = new CandidateViewRegistry();
1315
+ t.after(() => views.cleanupAll());
1316
+ const result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native, undefined, views);
1317
+ assert.equal(result.operation, "start");
1318
+ assert.equal(requests.length, 2, "provider-offered tree requires a second bound STATUS");
1319
+ assert.equal(requests[1]!.baseRef, baseTree, "transport preserves the offered tree selector");
1320
+ assert.equal(starts.length, 1);
1321
+ assert.equal(starts[0]!.baseRef, baseTree, "native START receives the provider's original tree selector");
1322
+ assert.equal(starts[0]!.targetIdentity, adopted.targetIdentity);
1323
+ for (const input of [undefined, { baseRef: baseCommit, committedOnly: true }, undefined]) {
1324
+ const before = requests.length;
1325
+ const next = await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "tree-offer-started", ...(input === undefined ? {} : { input: JSON.stringify(input) }) }, cwd, native, undefined, views);
1326
+ assert.equal(next.operation, "status");
1327
+ assert.equal(requests.length, before + 1);
1328
+ assert.equal(requests.at(-1)!.baseRef, input?.baseRef ?? baseTree, "selectorless STATUS must replay the provider tree; explicit caller commit wins only for its call");
1329
+ assert.equal(requests.at(-1)!.lineageId, "tree-offer-started");
1330
+ }
1331
+ });
1332
+
1333
+ test("facade STATUS hydrates an ordinary staged-index reviewer candidate without treating its base tree as an offered range", async (t) => {
1334
+ const cwd = repository(t);
1335
+ const baseTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd, encoding: "utf8" }).trim();
1336
+ writeFileSync(join(cwd, "tracked.txt"), "staged index candidate\n");
1337
+ execFileSync("git", ["add", "tracked.txt"], { cwd });
1338
+ const stagedTree = execFileSync("git", ["write-tree"], { cwd, encoding: "utf8" }).trim();
1339
+ const lineageId = "staged-index-hydration";
1340
+ const target = status(lineageId);
1341
+ target.projection = { ...target.projection, projection: "staged", baseTree, initialReviewTree: stagedTree, currentCandidateTree: stagedTree, paths: ["tracked.txt"] };
1342
+ const views = new CandidateViewRegistry();
1343
+ t.after(() => views.cleanupAll());
1344
+ const native = { targetStatus: async () => target } as unknown as NativeReviewCli;
1345
+ await __testing.executeReviewControllerOperation({ operation: "status", lineageId }, cwd, native, undefined, views);
1346
+ const restored = views.resolveCurrentForLens("review-risk", cwd);
1347
+ assert.equal(restored.baseTree, baseTree);
1348
+ assert.equal(restored.candidateTree, stagedTree);
1349
+ assert.equal(restored.committedOnly, false);
1350
+ assert.equal(views.resolveProjection(lineageId, cwd).providerBaseTree, undefined);
1351
+ });
1352
+
1353
+ test("fresh facade STATUS hydrates a historical commit-backed range without granting provider-tree provenance", async (t) => {
1354
+ const cwd = repository(t);
1355
+ const baseCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
1356
+ const baseTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd, encoding: "utf8" }).trim();
1357
+ writeFileSync(join(cwd, "tracked.txt"), "committed candidate\n");
1358
+ execFileSync("git", ["add", "tracked.txt"], { cwd });
1359
+ execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "candidate"], { cwd });
1360
+ const currentTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd, encoding: "utf8" }).trim();
1361
+ const historical = status("historical-commit-range");
1362
+ historical.projection = { ...historical.projection, projection: "staged", baseTree, initialReviewTree: currentTree, currentCandidateTree: currentTree, paths: ["tracked.txt"] };
1363
+ const views = new CandidateViewRegistry();
1364
+ t.after(() => views.cleanupAll());
1365
+ await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "historical-commit-range" }, cwd, { targetStatus: async () => historical } as unknown as NativeReviewCli, undefined, views);
1366
+ const restored = views.resolveCurrentForLens("review-risk", cwd);
1367
+ assert.equal(restored.baseCommit, baseCommit);
1368
+ assert.equal(restored.baseTree, baseTree);
1369
+ assert.equal(restored.candidateTree, currentTree);
1370
+ assert.equal(restored.committedOnly, true);
1371
+ assert.equal(views.resolveProjection("historical-commit-range", cwd).providerBaseTree, undefined);
1372
+ });
1373
+
1374
+ test("facade STATUS refuses an unreachable base tree and a false staged-index kind", async (t) => {
1375
+ const cwd = repository(t);
1376
+ const currentTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { cwd, encoding: "utf8" }).trim();
1377
+ const unreachableTree = execFileSync("git", ["mktree"], { cwd, input: "", encoding: "utf8" }).trim();
1378
+ assert.notEqual(unreachableTree, currentTree);
1379
+ const unknown = status("unprovable-tree");
1380
+ unknown.projection = { ...unknown.projection, projection: "staged", baseTree: unreachableTree, initialReviewTree: currentTree, currentCandidateTree: currentTree, paths: ["tracked.txt"] };
1381
+ const views = new CandidateViewRegistry();
1382
+ t.after(() => views.cleanupAll());
1383
+ await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "unprovable-tree" }, cwd, { targetStatus: async () => unknown } as unknown as NativeReviewCli, undefined, views);
1384
+ assert.equal(views.hasProjection("unprovable-tree", cwd), false);
1385
+ assert.throws(() => views.resolveCurrentForLens("review-risk", cwd), (error: unknown) => error instanceof CandidateViewError && error.reason === "current-binding-hydration-failed");
1386
+
1387
+ writeFileSync(join(cwd, "tracked.txt"), "staged index candidate\n");
1388
+ execFileSync("git", ["add", "tracked.txt"], { cwd });
1389
+ const stagedTree = execFileSync("git", ["write-tree"], { cwd, encoding: "utf8" }).trim();
1390
+ writeFileSync(join(cwd, "tracked.txt"), "uncommitted workspace content\n");
1391
+ const falseKind = status("false-staged-index");
1392
+ falseKind.projection = { ...falseKind.projection, projection: "staged", baseTree: currentTree, initialReviewTree: stagedTree, currentCandidateTree: stagedTree, paths: ["tracked.txt"] };
1393
+ const otherViews = new CandidateViewRegistry();
1394
+ t.after(() => otherViews.cleanupAll());
1395
+ await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "false-staged-index" }, cwd, { targetStatus: async () => falseKind } as unknown as NativeReviewCli, undefined, otherViews);
1396
+ assert.equal(otherViews.hasProjection("false-staged-index", cwd), false);
1397
+ assert.throws(() => otherViews.resolveCurrentForLens("review-risk", cwd), (error: unknown) => error instanceof CandidateViewError && error.reason === "current-binding-hydration-failed" && error.message.includes("candidate-view-invalid"));
1398
+ });
1399
+
1400
+ test("provider tree adoption refuses drifted targets, malformed offers and explicit caller trees", async (t) => {
1401
+ const cwd = repository(t);
1402
+ execFileSync("git", ["add", "tracked.txt"], { cwd });
1403
+ execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "candidate"], { cwd });
1404
+ const baseCommit = execFileSync("git", ["rev-parse", "HEAD~1"], { cwd, encoding: "utf8" }).trim();
1405
+ const baseTree = execFileSync("git", ["rev-parse", `${baseCommit}^{tree}`], { cwd, encoding: "utf8" }).trim();
1406
+ const adopted = startStatus(cwd, baseCommit);
1407
+ const offered = startStatus(cwd);
1408
+ offered.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [{ name: "base-ref", value: baseTree }, { name: "committed-only", value: "true" }], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
1409
+ const drifted = startStatus(cwd);
1410
+ const malformed = startStatus(cwd);
1411
+ malformed.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [{ name: "base-ref", value: baseTree }, { name: "base-ref", value: baseTree }, { name: "committed-only", value: "true" }], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
1412
+ const malformedExtra = startStatus(cwd);
1413
+ malformedExtra.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [
1414
+ { name: "agent", value: "pi" }, { name: "base-ref", value: baseTree, token: `--base-ref=${baseTree}` },
1415
+ { name: "committed-only", value: "true", token: "--committed-only=true" }, { name: "committed-only", value: "false" },
1416
+ ], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
1417
+ const malformedToken = startStatus(cwd);
1418
+ malformedToken.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [
1419
+ { name: "agent", value: "pi" }, { name: "base-ref", value: baseTree, token: "--base-ref=HEAD" },
1420
+ { name: "committed-only", value: "true", token: "--committed-only=true" },
1421
+ ], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
1422
+ const blob = execFileSync("git", ["rev-parse", "HEAD:tracked.txt"], { cwd, encoding: "utf8" }).trim();
1423
+ const wrongType = startStatus(cwd);
1424
+ wrongType.nextTransition = { kind: "execute", reasonCode: "start_required", execute: { operation: "review.start", arguments: [{ name: "base-ref", value: blob }, { name: "committed-only", value: "true" }], preconditions: [], binding: {} } } as unknown as ReviewStatusV3["nextTransition"];
1425
+ for (const scenario of [
1426
+ { name: "changed tree target", first: offered, second: drifted, expectedReads: 2 },
1427
+ { name: "malformed duplicate offer", first: malformed, second: adopted, expectedReads: 1 },
1428
+ { name: "conflicting selector among unrelated arguments", first: malformedExtra, second: adopted, expectedReads: 1 },
1429
+ { name: "selector token disagrees with value", first: malformedToken, second: adopted, expectedReads: 1 },
1430
+ { name: "wrong object type", first: wrongType, second: adopted, expectedReads: 2 },
1431
+ ]) {
1432
+ let reads = 0, starts = 0;
1433
+ const views = new CandidateViewRegistry();
1434
+ t.after(() => views.cleanupAll());
1435
+ const native = { targetStatus: async () => (++reads === 1 ? scenario.first : scenario.second), start: async () => { starts++; throw new Error("native START must not run"); } } as unknown as NativeReviewCli;
1436
+ const result = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native, undefined, views);
1437
+ assert.equal(reads, scenario.expectedReads, scenario.name);
1438
+ assert.equal(starts, 0, scenario.name);
1439
+ assert.equal(result.status, "blocked", scenario.name);
1440
+ }
1441
+ let reads = 0, starts = 0;
1442
+ const native = { targetStatus: async () => { reads++; return offered; }, start: async () => { starts++; throw new Error("native START must not run"); } } as unknown as NativeReviewCli;
1443
+ const rejected = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef: baseTree, committedOnly: true }) }, cwd, native);
1444
+ assert.equal(rejected.outcome, "native-start-base-ref-unresolvable");
1445
+ assert.equal(reads, 0);
1446
+ assert.equal(starts, 0);
1447
+ });
1448
+
1048
1449
  test("ordinary START preserves sanitized foreign diagnostics through one ambiguous reconciliation", async (t) => {
1049
1450
  const cwd = repository(t);
1050
1451
  const target = startStatus(cwd);
@@ -1160,6 +1561,60 @@ for (const statusSchema of ["gentle-ai.review-integration.status/v6", "gentle-ai
1160
1561
  assert.equal(requests.every((request) => !("lineageId" in request)), true);
1161
1562
  });
1162
1563
 
1564
+ test("committed-range inspect stop carries its binding selector into selection STATUS and START, rejecting a mismatched binding", async (t) => {
1565
+ const { cwd, eligible, selection } = untrackedStopFixture(t);
1566
+ const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
1567
+ const initialTarget = startStatus(cwd, baseRef), target = startStatus(cwd, baseRef, [eligible]);
1568
+ const boundSelection = { ...selection, arguments: selection.arguments.map((arg) => arg.name === "base_tree" ? { ...arg, value: initialTarget.projection.baseTree } : arg.name === "candidate_tree" ? { ...arg, value: initialTarget.projection.currentCandidateTree } : arg) };
1569
+ const initial = { ...initialTarget, nextTransition: { kind: "collect", reasonCode: "intended_untracked_selection_required", collect: { inputs: [boundSelection] } }, raw: { schema: "gentle-ai.review-integration.status/v7" } } as ReviewStatusV3;
1570
+ const requests: Array<Record<string, unknown>> = [], starts: Array<Record<string, unknown>> = [], retained = new Map();
1571
+ const native = {
1572
+ targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return "intendedUntrackedSelection" in request ? target : initial; },
1573
+ start: async (request: Record<string, unknown>) => { starts.push(request); return { lineageId: "review-selected", state: "reviewing", riskLevel: "low", selectedLenses: [], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false, riskReasons: [], raw: {} }; },
1574
+ } as unknown as NativeReviewCli;
1575
+ const inspected = await __testing.executeReviewControllerOperation({ operation: "inspect", input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }) }, cwd, native, undefined, undefined, undefined, retained);
1576
+ assert.equal(typeof inspected.selectionBinding, "string");
1577
+ assert.equal(retained.has(`${cwd}\u0000`), true, "plain inspect stop retains its selector");
1578
+ const mismatch = await __testing.executeReviewControllerOperation({ operation: "select-intended-untracked", selectionBinding: (inspected.selectionBinding as string).replace(eligible, "other.md"), intendedUntracked: [eligible] }, cwd, native, undefined, undefined, undefined, retained);
1579
+ assert.equal(mismatch.outcome, "intended-untracked-selection-binding-rejected");
1580
+ assert.equal(requests.length, 1, "mismatched binding must not request another STATUS");
1581
+ assert.equal(starts.length, 0);
1582
+ const selected = await __testing.executeReviewControllerOperation({ operation: "select-intended-untracked", selectionBinding: inspected.selectionBinding, intendedUntracked: [eligible] }, cwd, native, undefined, undefined, undefined, retained);
1583
+ assert.equal(selected.operation, "select-intended-untracked");
1584
+ assert.equal(starts.length, 1, JSON.stringify(selected));
1585
+ assert.equal(requests.length, 3);
1586
+ for (const request of requests) { assert.equal(request.baseRef, baseRef); assert.equal(request.committedOnly, true); }
1587
+ assert.equal(starts[0]!.baseRef, baseRef);
1588
+ assert.equal(starts[0]!.committedOnly, true);
1589
+ });
1590
+
1591
+ test("committed-range selection rejects target-identity drift before START", async (t) => {
1592
+ const { cwd, eligible, selection } = untrackedStopFixture(t);
1593
+ const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
1594
+ const initialTarget = startStatus(cwd, baseRef);
1595
+ const boundSelection = { ...selection, arguments: selection.arguments.map((arg) => arg.name === "base_tree" ? { ...arg, value: initialTarget.projection.baseTree } : arg.name === "candidate_tree" ? { ...arg, value: initialTarget.projection.currentCandidateTree } : arg) };
1596
+ const initial = { ...initialTarget, nextTransition: { kind: "collect", reasonCode: "intended_untracked_selection_required", collect: { inputs: [boundSelection] } }, raw: { schema: "gentle-ai.review-integration.status/v7" } } as ReviewStatusV3;
1597
+ const changed = { ...initial, targetIdentity: `sha256:${"d".repeat(64)}` } as ReviewStatusV3;
1598
+ const requests: Array<Record<string, unknown>> = [];
1599
+ let starts = 0;
1600
+ const retained = new Map();
1601
+ const native = {
1602
+ targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return requests.length === 1 ? initial : changed; },
1603
+ start: async () => { starts++; throw new Error("START must not run for a changed target"); },
1604
+ } as unknown as NativeReviewCli;
1605
+ const inspected = await __testing.executeReviewControllerOperation({ operation: "inspect", input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }) }, cwd, native, undefined, undefined, undefined, retained);
1606
+ assert.equal(typeof inspected.selectionBinding, "string");
1607
+ const rejected = await __testing.executeReviewControllerOperation({ operation: "select-intended-untracked", selectionBinding: inspected.selectionBinding, intendedUntracked: [eligible] }, cwd, native, undefined, undefined, undefined, retained);
1608
+ assert.equal(rejected.status, "blocked");
1609
+ assert.equal(rejected.outcome, "intended-untracked-selection-binding-rejected");
1610
+ assert.equal(rejected.mutation_performed, false);
1611
+ assert.equal(rejected.mutation_outcome, "none");
1612
+ assert.equal(requests.length, 2);
1613
+ assert.equal(requests[1]!.baseRef, baseRef);
1614
+ assert.equal(requests[1]!.committedOnly, true);
1615
+ assert.equal(starts, 0);
1616
+ });
1617
+
1163
1618
  // gentle-pi#706: inspect names the exact continuation for the intended-untracked
1164
1619
  // stop and can resolve it in one call through top-level untrackedScope.
1165
1620
  function untrackedStopFixture(
@@ -1467,6 +1922,7 @@ test("plain START adopts the pre-lineage selection retained by inspect and delet
1467
1922
  retained,
1468
1923
  );
1469
1924
  assert.equal(started.operation, "start");
1925
+ assert.ok(typeof started.result === "object" && started.result !== null && "lineage_id" in started.result);
1470
1926
  assert.equal(started.result.lineage_id, "review-started");
1471
1927
  assert.equal(starts.length, 1);
1472
1928
  assert.equal(requests.length, 3);
@@ -1562,6 +2018,300 @@ test("a fresh unsuccessful inspect invalidates its prior pre-lineage selection",
1562
2018
  assert.equal(retained.has(`${cwd}\u0000`), false);
1563
2019
  });
1564
2020
 
2021
+ // gentle-pi#1192: a committed-range inspect's retained pre-lineage selection
2022
+ // must remember its own baseRef/committedOnly too, so a later plain START
2023
+ // replays the same committed range instead of falling back to the
2024
+ // workspace-diff one.
2025
+ test("inspect with a committed-range selector retains baseRef/committedOnly, and a plain START adopts it", async (t) => {
2026
+ const cwd = repository(t), eligible = "selected.md";
2027
+ writeFileSync(join(cwd, eligible), "selected\n");
2028
+ const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
2029
+ // The inspect handler carries its committed-range selector through both the
2030
+ // pre-resolution and resolution STATUS calls, so a faithful fixture for
2031
+ // this scenario must project the committed range on both, not the
2032
+ // workspace-diff range untrackedStopFixture builds.
2033
+ const initialTarget = startStatus(cwd, baseRef),
2034
+ target = startStatus(cwd, baseRef, [eligible]);
2035
+ const selection: ReviewCollectInputV3 = {
2036
+ name: "intended_untracked_selection",
2037
+ schema: "gentle-ai.review-intended-untracked-selection/v1",
2038
+ captureOperation: "external.select_intended_untracked",
2039
+ arguments: [
2040
+ { name: "target_identity", value: SHA },
2041
+ { name: "projection", value: "workspace" },
2042
+ { name: "base_tree", value: initialTarget.projection.baseTree },
2043
+ { name: "candidate_tree", value: initialTarget.projection.currentCandidateTree },
2044
+ { name: "eligible_paths_json", value: JSON.stringify([eligible]) },
2045
+ { name: "expected_untracked_inventory", value: SHA },
2046
+ ],
2047
+ submission: {
2048
+ operationToken: "status",
2049
+ argumentTokens: [
2050
+ "--contract=gentle-ai.review-integration/v2",
2051
+ "--next-transition=true",
2052
+ "--agent=pi",
2053
+ "--projection=workspace",
2054
+ "--intended-untracked-selection={{value}}",
2055
+ ],
2056
+ values: [
2057
+ {
2058
+ slot: "intended_untracked_selection",
2059
+ domain: "schema_bound_json",
2060
+ schema: "gentle-ai.review-intended-untracked-selection/v1",
2061
+ substitutionLocation: 4,
2062
+ },
2063
+ ],
2064
+ },
2065
+ };
2066
+ const initial = {
2067
+ ...initialTarget,
2068
+ nextTransition: {
2069
+ kind: "collect",
2070
+ reasonCode: "intended_untracked_selection_required",
2071
+ collect: { inputs: [selection] },
2072
+ },
2073
+ raw: { schema: "gentle-ai.review-integration.status/v7" },
2074
+ } as ReviewStatusV3;
2075
+ const requests: Array<Record<string, unknown>> = [],
2076
+ starts: Array<Record<string, unknown>> = [],
2077
+ retained = new Map();
2078
+ const native = {
2079
+ targetStatus: async (request: Record<string, unknown>) => {
2080
+ requests.push(request);
2081
+ return "intendedUntrackedSelection" in request ? target : initial;
2082
+ },
2083
+ start: async (request: Record<string, unknown>) => {
2084
+ starts.push(request);
2085
+ return {
2086
+ lineageId: "review-started", state: "reviewing", riskLevel: "low", selectedLenses: [],
2087
+ changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
2088
+ riskReasons: [], raw: {},
2089
+ };
2090
+ },
2091
+ } as unknown as NativeReviewCli;
2092
+ const resolved = await __testing.executeReviewControllerOperation(
2093
+ {
2094
+ operation: "inspect",
2095
+ input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }),
2096
+ untrackedScope: "select",
2097
+ intendedUntracked: [eligible],
2098
+ },
2099
+ cwd, native, undefined, undefined, undefined, retained,
2100
+ );
2101
+ assert.equal(resolved.status, "ready");
2102
+ const retainedEntry = retained.get(`${cwd}\u0000`) as { baseRef?: string; committedOnly?: true } | undefined;
2103
+ assert.notEqual(retainedEntry, undefined);
2104
+ assert.equal(retainedEntry!.baseRef, baseRef);
2105
+ assert.equal(retainedEntry!.committedOnly, true);
2106
+
2107
+ const started = await __testing.executeReviewControllerOperation(
2108
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
2109
+ cwd, native, undefined, undefined, undefined, retained,
2110
+ );
2111
+ assert.equal(started.operation, "start");
2112
+ assert.equal((started.result as { lineage_id?: string }).lineage_id, "review-started");
2113
+ assert.equal(requests.length, 3);
2114
+ const startStatusRequest = requests[2]!;
2115
+ assert.equal(startStatusRequest.baseRef, baseRef);
2116
+ assert.equal(startStatusRequest.committedOnly, true);
2117
+ assert.equal(startStatusRequest.untrackedScope, "select");
2118
+ assert.deepEqual(startStatusRequest.intendedUntrackedSelection, {
2119
+ argumentTokens: selection.submission!.argumentTokens,
2120
+ value: JSON.stringify({
2121
+ schema: "gentle-ai.review-intended-untracked-selection/v1",
2122
+ untracked_scope: "select",
2123
+ expected_untracked_inventory: SHA,
2124
+ intended_untracked: [eligible],
2125
+ }),
2126
+ });
2127
+ assert.equal(starts.length, 1);
2128
+ assert.equal(starts[0]!.baseRef, baseRef);
2129
+ assert.equal(starts[0]!.committedOnly, true);
2130
+ assert.equal(starts[0]!.untrackedScope, "select");
2131
+ assert.equal(retained.has(`${cwd}\u0000`), false);
2132
+ const lineageEntry = retained.get(`${cwd}\u0000review-started`) as { untrackedScope: string; baseRef?: string } | undefined;
2133
+ assert.notEqual(lineageEntry, undefined);
2134
+ assert.equal(lineageEntry!.untrackedScope, "select");
2135
+ // The lineage-scoped entry must stay a plain untracked selection: a
2136
+ // leftover baseRef field would make readRetainedNativeUntrackedSelection
2137
+ // mistake it for a RetainedNativeCaptureRoute.
2138
+ assert.equal("baseRef" in lineageEntry!, false);
2139
+ });
2140
+
2141
+ // gentle-pi#review-ready-inspect-selector: a plain inspect that resolves ready
2142
+ // without ever needing an untrackedScope decision must still retain its own
2143
+ // committed-range selector, so the following plain START replays the exact
2144
+ // inspected range instead of adopting the native default base-ref (#874).
2145
+ test("a plain ready inspect retains its committed selector, and a plain START adopts it", async (t) => {
2146
+ const cwd = repository(t);
2147
+ const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
2148
+ const target = startStatus(cwd, baseRef);
2149
+ const requests: Array<Record<string, unknown>> = [],
2150
+ starts: Array<Record<string, unknown>> = [],
2151
+ retained = new Map();
2152
+ const native = {
2153
+ targetStatus: async (request: Record<string, unknown>) => { requests.push(request); return target; },
2154
+ start: async (request: Record<string, unknown>) => {
2155
+ starts.push(request);
2156
+ return {
2157
+ lineageId: "ready-started", state: "reviewing", riskLevel: "low", selectedLenses: [],
2158
+ changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
2159
+ riskReasons: [], raw: {},
2160
+ };
2161
+ },
2162
+ } as unknown as NativeReviewCli;
2163
+ const inspected = await __testing.executeReviewControllerOperation(
2164
+ { operation: "inspect", input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }) },
2165
+ cwd, native, undefined, undefined, undefined, retained,
2166
+ );
2167
+ assert.equal(inspected.status, "ready");
2168
+ const retainedEntry = retained.get(`${cwd}\u0000`) as { baseRef?: string; committedOnly?: true } | undefined;
2169
+ assert.notEqual(retainedEntry, undefined);
2170
+ assert.equal(retainedEntry!.baseRef, baseRef);
2171
+ assert.equal(retainedEntry!.committedOnly, true);
2172
+
2173
+ const started = await __testing.executeReviewControllerOperation(
2174
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
2175
+ cwd, native, undefined, undefined, undefined, retained,
2176
+ );
2177
+ assert.equal(started.operation, "start");
2178
+ assert.equal((started.result as { lineage_id?: string }).lineage_id, "ready-started");
2179
+ assert.equal(requests.length, 2);
2180
+ const startStatusRequest = requests[1]!;
2181
+ assert.equal(startStatusRequest.baseRef, baseRef);
2182
+ assert.equal(startStatusRequest.committedOnly, true);
2183
+ assert.equal(starts.length, 1);
2184
+ assert.equal(starts[0]!.baseRef, baseRef);
2185
+ assert.equal(starts[0]!.committedOnly, true);
2186
+ assert.equal(retained.has(`${cwd}\u0000`), false);
2187
+ });
2188
+
2189
+ test("a later ready inspect with a different baseRef replaces the earlier retained selector", async (t) => {
2190
+ const cwd = repository(t);
2191
+ const baseRef1 = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
2192
+ const target1 = startStatus(cwd, baseRef1);
2193
+ execFileSync("git", ["add", "tracked.txt"], { cwd, stdio: "ignore" });
2194
+ execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "second"], { cwd, stdio: "ignore" });
2195
+ const baseRef2 = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
2196
+ const target2 = startStatus(cwd, baseRef2);
2197
+ const starts: Array<Record<string, unknown>> = [],
2198
+ retained = new Map();
2199
+ const native = {
2200
+ targetStatus: async (request: Record<string, unknown>) => (request.baseRef === baseRef2 ? target2 : target1),
2201
+ start: async (request: Record<string, unknown>) => {
2202
+ starts.push(request);
2203
+ return {
2204
+ lineageId: "second-started", state: "reviewing", riskLevel: "low", selectedLenses: [],
2205
+ changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
2206
+ riskReasons: [], raw: {},
2207
+ };
2208
+ },
2209
+ } as unknown as NativeReviewCli;
2210
+ await __testing.executeReviewControllerOperation(
2211
+ { operation: "inspect", input: JSON.stringify({ baseRef: baseRef1, committedOnly: true }) },
2212
+ cwd, native, undefined, undefined, undefined, retained,
2213
+ );
2214
+ await __testing.executeReviewControllerOperation(
2215
+ { operation: "inspect", input: JSON.stringify({ baseRef: baseRef2, committedOnly: true }) },
2216
+ cwd, native, undefined, undefined, undefined, retained,
2217
+ );
2218
+ const retainedEntry = retained.get(`${cwd}\u0000`) as { baseRef?: string } | undefined;
2219
+ assert.equal(retainedEntry?.baseRef, baseRef2);
2220
+
2221
+ const started = await __testing.executeReviewControllerOperation(
2222
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
2223
+ cwd, native, undefined, undefined, undefined, retained,
2224
+ );
2225
+ assert.equal((started.result as { lineage_id?: string }).lineage_id, "second-started");
2226
+ assert.equal(starts.length, 1);
2227
+ assert.equal(starts[0]!.baseRef, baseRef2);
2228
+ });
2229
+
2230
+ test("START rejects a ready-inspect retained selector when fresh STATUS identifies a changed candidate", async (t) => {
2231
+ const cwd = repository(t);
2232
+ const baseRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
2233
+ const candidateA = startStatus(cwd, baseRef);
2234
+ const candidateB = {
2235
+ ...candidateA,
2236
+ targetIdentity: "target-b",
2237
+ projection: { ...candidateA.projection, currentCandidateTree: "candidate-b" },
2238
+ } as ReviewStatusV3;
2239
+ const retained = new Map();
2240
+ let statusCalls = 0;
2241
+ let starts = 0;
2242
+ const native = {
2243
+ targetStatus: async () => { statusCalls += 1; return statusCalls === 1 ? candidateA : candidateB; },
2244
+ start: async () => {
2245
+ starts += 1;
2246
+ return {
2247
+ lineageId: "unexpected", state: "reviewing", riskLevel: "low", selectedLenses: [],
2248
+ changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
2249
+ riskReasons: [], raw: {},
2250
+ };
2251
+ },
2252
+ } as unknown as NativeReviewCli;
2253
+ await __testing.executeReviewControllerOperation(
2254
+ { operation: "inspect", input: JSON.stringify({ baseRef: "HEAD", committedOnly: true }) },
2255
+ cwd, native, undefined, null, undefined, retained,
2256
+ );
2257
+ const rejected = await __testing.executeReviewControllerOperation(
2258
+ { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
2259
+ cwd, native, undefined, null, undefined, retained,
2260
+ );
2261
+ assert.equal(rejected.outcome, "native-start-retained-selection-candidate-mismatch");
2262
+ assert.equal(starts, 0);
2263
+ assert.equal(retained.has(`${cwd}\u0000`), false);
2264
+ });
2265
+
2266
+ test("an explicit baseRef that conflicts with a retained committed-range selector is not adopted", async (t) => {
2267
+ const { cwd, eligible, initial, target } = untrackedStopFixture(t);
2268
+ const headRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
2269
+ writeFileSync(join(cwd, "tracked.txt"), "second\n");
2270
+ execFileSync("git", ["add", "tracked.txt"], { cwd, stdio: "ignore" });
2271
+ execFileSync("git", ["-c", "user.name=Routing Test", "-c", "user.email=routing@example.invalid", "commit", "-m", "second"], { cwd, stdio: "ignore" });
2272
+ const otherRef = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
2273
+ const readyTarget = startStatus(cwd, otherRef);
2274
+ const starts: Array<Record<string, unknown>> = [],
2275
+ retained = new Map();
2276
+ const native = {
2277
+ targetStatus: async (request: Record<string, unknown>) =>
2278
+ request.baseRef === otherRef ? readyTarget : ("intendedUntrackedSelection" in request ? target : initial),
2279
+ start: async (request: Record<string, unknown>) => {
2280
+ starts.push(request);
2281
+ return {
2282
+ lineageId: "review-started", state: "reviewing", riskLevel: "low", selectedLenses: [],
2283
+ changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: false,
2284
+ riskReasons: [], raw: {},
2285
+ };
2286
+ },
2287
+ } as unknown as NativeReviewCli;
2288
+ await __testing.executeReviewControllerOperation(
2289
+ {
2290
+ operation: "inspect",
2291
+ input: JSON.stringify({ baseRef: headRef, committedOnly: true }),
2292
+ untrackedScope: "select",
2293
+ intendedUntracked: [eligible],
2294
+ },
2295
+ cwd, native, undefined, undefined, undefined, retained,
2296
+ );
2297
+ const retainedEntry = retained.get(`${cwd}\u0000`) as { baseRef?: string } | undefined;
2298
+ assert.equal(retainedEntry?.baseRef, headRef);
2299
+
2300
+ const started = await __testing.executeReviewControllerOperation(
2301
+ { operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef: otherRef, committedOnly: true }) },
2302
+ cwd, native, undefined, undefined, undefined, retained,
2303
+ );
2304
+ assert.equal(started.operation, "start");
2305
+ assert.equal(starts.length, 1);
2306
+ assert.equal(starts[0]!.baseRef, otherRef);
2307
+ // The conflicting explicit START never adopts the retained scope/inventory.
2308
+ assert.equal("untrackedScope" in starts[0]!, false);
2309
+ // A successful START always clears the pre-lineage "" entry (gentle-pi#706),
2310
+ // whether or not it consumed it: a new lineage invalidates any stale
2311
+ // pre-lineage selection regardless of this START's own adoption choice.
2312
+ assert.equal(retained.has(`${cwd}\u0000`), false);
2313
+ });
2314
+
1565
2315
  test("START rejects a retained pre-lineage selection when fresh STATUS identifies a changed candidate", async (t) => {
1566
2316
  const { cwd, eligible, initial, target } = untrackedStopFixture(t);
1567
2317
  const retained = new Map();
@@ -1628,6 +2378,7 @@ test("a failed START retains a pre-lineage selection for a same-candidate retry"
1628
2378
  { operation: "start", input: JSON.stringify({ mode: "ordinary" }) },
1629
2379
  cwd, native, undefined, null, undefined, retained,
1630
2380
  );
2381
+ assert.ok(typeof retried.result === "object" && retried.result !== null && "lineage_id" in retried.result);
1631
2382
  assert.equal(retried.result.lineage_id, "retried");
1632
2383
  assert.equal(starts, 2);
1633
2384
  assert.equal(retained.has(`${cwd}\u0000`), false);
@@ -1833,6 +2584,115 @@ test("targeted-validator provider vectors preserve their nonuniform native closu
1833
2584
  assert.equal("correction_target_identity" in result, false);
1834
2585
  });
1835
2586
 
2587
+ test("approved capture closure envelope carries additive next_action alongside untouched raw acknowledgement", async () => {
2588
+ const closureLineage = "review-approved-closure-next-action";
2589
+ const input = {
2590
+ name: "provider_targeted_validator",
2591
+ schema: "https://gentle-ai.dev/schema/review/targeted-validator/v1",
2592
+ captureOperation: "review.capture-validation",
2593
+ arguments: [
2594
+ { name: "lineage", value: closureLineage, token: `--lineage=${closureLineage}` },
2595
+ { name: "target", value: SHA, token: `--target=${SHA}` },
2596
+ { name: "agent", value: "pi", token: "--agent=pi" },
2597
+ { name: "execute", value: "true", token: "--execute=true" },
2598
+ ],
2599
+ } as unknown as ReviewCollectInputV3;
2600
+ const roleStatus = { ...status(closureLineage), nextTransition: { kind: "collect", reasonCode: "provider_role_required", collect: { inputs: [input] } } } as ReviewStatusV3;
2601
+ const ackContinuation = {
2602
+ operation: "review.acknowledge-approved",
2603
+ command: "gentle-ai review acknowledge-approved --provider-vector",
2604
+ arguments: [
2605
+ { name: "cwd", value: process.cwd(), token: `--cwd=${process.cwd()}` },
2606
+ { name: "lineage", value: closureLineage, token: `--lineage=${closureLineage}` },
2607
+ { name: "target", value: SHA, token: `--target=${SHA}` },
2608
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
2609
+ { name: "token", value: "provider-tok", token: "--token=provider-tok" },
2610
+ ],
2611
+ preconditions: [{ name: "state", value: "approved" }],
2612
+ binding: { lineageId: closureLineage, revision: SHA, targetIdentity: SHA },
2613
+ };
2614
+ const native = {
2615
+ targetStatus: async () => roleStatus,
2616
+ captureProviderRole: async () => ({
2617
+ schema: "gentle-ai.review-last-event-closure/v1",
2618
+ operation: "review/capture-validation",
2619
+ lineageId: closureLineage,
2620
+ state: "approved",
2621
+ action: "approved by validator",
2622
+ storeRevision: SHA,
2623
+ acknowledgement: {
2624
+ operation: "review.acknowledge-approved",
2625
+ command: "gentle-ai review acknowledge-approved --provider-vector",
2626
+ arguments: ackContinuation.arguments,
2627
+ preconditions: ackContinuation.preconditions,
2628
+ binding: ackContinuation.binding,
2629
+ raw: ackContinuation,
2630
+ },
2631
+ }),
2632
+ } as unknown as NativeReviewCli;
2633
+ const result = await __testing.executeReviewCaptureOperation({ lineageId: closureLineage, collectBinding: JSON.stringify(input) }, process.cwd(), native);
2634
+ assert.equal(result.status, "closed");
2635
+ assert.equal(result.outcome, "native-last-event-closure");
2636
+ assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${closureLineage}"}`);
2637
+ assert.equal((result.closure as { next_action?: string }).next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${closureLineage}"}`);
2638
+ assert.deepEqual((result.closure as { acknowledgement?: unknown }).acknowledgement, ackContinuation);
2639
+ });
2640
+
2641
+ test("approved capture closure envelope preserves workspaceRoot in next_action when distinct from process cwd", async (t) => {
2642
+ const closureLineage = "review-approved-closure-next-action-worktree";
2643
+ const worktreeRoot = repository(t);
2644
+ const input = {
2645
+ name: "provider_targeted_validator",
2646
+ schema: "https://gentle-ai.dev/schema/review/targeted-validator/v1",
2647
+ captureOperation: "review.capture-validation",
2648
+ arguments: [
2649
+ { name: "lineage", value: closureLineage, token: `--lineage=${closureLineage}` },
2650
+ { name: "target", value: SHA, token: `--target=${SHA}` },
2651
+ { name: "agent", value: "pi", token: "--agent=pi" },
2652
+ { name: "execute", value: "true", token: "--execute=true" },
2653
+ ],
2654
+ } as unknown as ReviewCollectInputV3;
2655
+ const roleStatus = { ...status(closureLineage), nextTransition: { kind: "collect", reasonCode: "provider_role_required", collect: { inputs: [input] } } } as ReviewStatusV3;
2656
+ const ackContinuation = {
2657
+ operation: "review.acknowledge-approved",
2658
+ command: "gentle-ai review acknowledge-approved --provider-vector",
2659
+ arguments: [
2660
+ { name: "cwd", value: worktreeRoot, token: `--cwd=${worktreeRoot}` },
2661
+ { name: "lineage", value: closureLineage, token: `--lineage=${closureLineage}` },
2662
+ { name: "target", value: SHA, token: `--target=${SHA}` },
2663
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
2664
+ { name: "token", value: "provider-tok", token: "--token=provider-tok" },
2665
+ ],
2666
+ preconditions: [{ name: "state", value: "approved" }],
2667
+ binding: { lineageId: closureLineage, revision: SHA, targetIdentity: SHA },
2668
+ };
2669
+ const native = {
2670
+ targetStatus: async () => roleStatus,
2671
+ captureProviderRole: async () => ({
2672
+ schema: "gentle-ai.review-last-event-closure/v1",
2673
+ operation: "review/capture-validation",
2674
+ lineageId: closureLineage,
2675
+ state: "approved",
2676
+ action: "approved by validator",
2677
+ storeRevision: SHA,
2678
+ acknowledgement: {
2679
+ operation: "review.acknowledge-approved",
2680
+ command: "gentle-ai review acknowledge-approved --provider-vector",
2681
+ arguments: ackContinuation.arguments,
2682
+ preconditions: ackContinuation.preconditions,
2683
+ binding: ackContinuation.binding,
2684
+ raw: ackContinuation,
2685
+ },
2686
+ }),
2687
+ } as unknown as NativeReviewCli;
2688
+ const result = await __testing.executeReviewCaptureOperation({ lineageId: closureLineage, collectBinding: JSON.stringify(input), workspaceRoot: worktreeRoot }, worktreeRoot, native);
2689
+ assert.equal(result.status, "closed");
2690
+ assert.equal(result.outcome, "native-last-event-closure");
2691
+ assert.equal(result.next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${closureLineage}","workspaceRoot":${JSON.stringify(worktreeRoot)}}`);
2692
+ assert.equal((result.closure as { next_action?: string }).next_action, `gentle_review {"operation":"acknowledge-approved","lineageId":"${closureLineage}","workspaceRoot":${JSON.stringify(worktreeRoot)}}`);
2693
+ assert.deepEqual((result.closure as { acknowledgement?: unknown }).acknowledgement, ackContinuation);
2694
+ });
2695
+
1836
2696
  test("targeted-validator captures echo the distinct provider correction target identity", async () => {
1837
2697
  const closureLineage = "review-validator-correction-target";
1838
2698
  const correctionTarget = `sha256:${"d".repeat(64)}`;