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
@@ -1,11 +1,11 @@
1
1
  import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, utimesSync } from "node:fs";
3
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, utimesSync } from "node:fs";
4
4
  import { mkdtempSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { gunzipSync, gzipSync } from "node:zlib";
8
- import { assertCandidateOwnerParent, createCandidateOwner, prepareCandidateOwnerParent, removeCandidateOwner, samePath, sweepCandidateOwners, type CandidateViewOwner } from "./review-candidate-view-owner.ts";
8
+ import { assertCandidateOwnerParent, createCandidateOwner, PosixCandidateOwnerParentChmodIneffectiveError, PosixCandidateOwnerParentPrivacyError, prepareCandidateOwnerParent, removeCandidateOwner, samePath, sweepCandidateOwners, type CandidateViewOwner } from "./review-candidate-view-owner.ts";
9
9
 
10
10
  const REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
11
11
  export type ReviewLens = (typeof REVIEW_LENS)[number];
@@ -108,6 +108,7 @@ interface CandidateViewRecord {
108
108
  commonDir: string;
109
109
  baseCommit: string;
110
110
  baseTree: string;
111
+ providerBaseTree?: string;
111
112
  candidateTree: string;
112
113
  committedOnly: boolean;
113
114
  intendedUntracked?: readonly string[];
@@ -125,6 +126,7 @@ export interface CandidateView {
125
126
  contributorRoot: string;
126
127
  baseCommit: string;
127
128
  baseTree: string;
129
+ providerBaseTree?: string;
128
130
  candidateTree: string;
129
131
  committedOnly: boolean;
130
132
  intendedUntracked?: readonly string[];
@@ -167,6 +169,7 @@ export interface FrozenCandidateProjection {
167
169
  contributorRoot: string;
168
170
  baseCommit: string;
169
171
  baseTree: string;
172
+ providerBaseTree?: string;
170
173
  candidateTree: string;
171
174
  committedOnly: boolean;
172
175
  intendedUntracked?: readonly string[];
@@ -179,6 +182,8 @@ export interface FrozenCandidateProjection {
179
182
  export interface CreateCandidateViewRequest {
180
183
  contributorRoot: string;
181
184
  baseRef?: string;
185
+ /** Provider-validated tree, distinct from caller-supplied commit/ref baseRef. */
186
+ providerBaseTree?: string;
182
187
  committedOnly?: boolean;
183
188
  /** Undefined keeps legacy all-untracked capture; [] excludes untracked files. */
184
189
  intendedUntracked?: readonly string[];
@@ -196,6 +201,7 @@ export interface AuthoritativeReviewingCandidateState {
196
201
  contributorRoot: string;
197
202
  baseCommit: string;
198
203
  baseTree: string;
204
+ providerBaseTree?: string;
199
205
  candidateTree: string;
200
206
  committedOnly?: boolean;
201
207
  intendedUntracked?: readonly string[];
@@ -226,6 +232,19 @@ export interface NativeCandidateProjectionDescriptor {
226
232
  providerManifestHashVerified?: true;
227
233
  }
228
234
 
235
+ export interface CandidateOwnerPrivacyDiagnostic {
236
+ code: "candidate-owner-parent-privacy";
237
+ message: typeof CANDIDATE_OWNER_PARENT_PRIVACY_MESSAGE;
238
+ }
239
+
240
+ export interface CandidateOwnerChmodIneffectiveDiagnostic {
241
+ code: "candidate-owner-parent-chmod-ineffective";
242
+ message: typeof CANDIDATE_OWNER_PARENT_CHMOD_INEFFECTIVE_MESSAGE;
243
+ }
244
+
245
+ const CANDIDATE_OWNER_PARENT_PRIVACY_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" as const;
246
+ const CANDIDATE_OWNER_PARENT_CHMOD_INEFFECTIVE_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" as const;
247
+
229
248
  export interface CandidateViewDiagnostic {
230
249
  phase: typeof CANDIDATE_VIEW_DIAGNOSTIC_PHASE;
231
250
  category: CandidateViewGitFailureCategory;
@@ -237,8 +256,8 @@ export interface CandidateViewDiagnostic {
237
256
 
238
257
  export class CandidateViewError extends Error {
239
258
  readonly reason: string;
240
- readonly diagnostics?: CandidateViewDiagnostic;
241
- constructor(message: string, reason = "candidate-view-invalid", diagnostics?: CandidateViewDiagnostic, options?: ErrorOptions) {
259
+ readonly diagnostics?: CandidateViewDiagnostic | CandidateOwnerPrivacyDiagnostic | CandidateOwnerChmodIneffectiveDiagnostic;
260
+ constructor(message: string, reason = "candidate-view-invalid", diagnostics?: CandidateViewDiagnostic | CandidateOwnerPrivacyDiagnostic | CandidateOwnerChmodIneffectiveDiagnostic, options?: ErrorOptions) {
242
261
  super(message, options);
243
262
  this.name = "CandidateViewError";
244
263
  this.reason = reason;
@@ -281,7 +300,12 @@ function candidateGitDiagnostic(category: CandidateViewGitFailureCategory, argum
281
300
  });
282
301
  }
283
302
 
284
- function sanitizeCandidateViewDiagnostic(diagnostics: CandidateViewDiagnostic): CandidateViewDiagnostic | undefined {
303
+ function sanitizeCandidateViewDiagnostic(diagnostics: CandidateViewDiagnostic | CandidateOwnerPrivacyDiagnostic | CandidateOwnerChmodIneffectiveDiagnostic): CandidateViewDiagnostic | CandidateOwnerPrivacyDiagnostic | CandidateOwnerChmodIneffectiveDiagnostic | undefined {
304
+ if ("code" in diagnostics) {
305
+ if (diagnostics.code === "candidate-owner-parent-privacy" && diagnostics.message === CANDIDATE_OWNER_PARENT_PRIVACY_MESSAGE) return Object.freeze({ code: diagnostics.code, message: CANDIDATE_OWNER_PARENT_PRIVACY_MESSAGE });
306
+ if (diagnostics.code === "candidate-owner-parent-chmod-ineffective" && diagnostics.message === CANDIDATE_OWNER_PARENT_CHMOD_INEFFECTIVE_MESSAGE) return Object.freeze({ code: diagnostics.code, message: CANDIDATE_OWNER_PARENT_CHMOD_INEFFECTIVE_MESSAGE });
307
+ return undefined;
308
+ }
285
309
  const { phase, category, git_subcommand, timeout_ms, max_buffer_bytes, message } = diagnostics;
286
310
  if (
287
311
  phase !== CANDIDATE_VIEW_DIAGNOSTIC_PHASE ||
@@ -362,6 +386,7 @@ function candidateRecordsShareIdentity(left: CandidateViewRecord, right: Candida
362
386
  return left.contributorRoot === right.contributorRoot &&
363
387
  left.baseCommit === right.baseCommit &&
364
388
  left.baseTree === right.baseTree &&
389
+ left.providerBaseTree === right.providerBaseTree &&
365
390
  left.candidateTree === right.candidateTree &&
366
391
  left.committedOnly === right.committedOnly &&
367
392
  JSON.stringify(left.intendedUntracked ?? null) === JSON.stringify(right.intendedUntracked ?? null) &&
@@ -593,7 +618,9 @@ function entryContentHash(root: string, entry: CandidateTreeEntry): string {
593
618
  if (entry.mode === "120000") {
594
619
  if (!item.isSymbolicLink()) throw new CandidateViewError("candidate view symlink does not match its frozen tree");
595
620
  const target = readlinkSync(path, "buffer");
596
- const bytes = Buffer.isBuffer(target) ? target : Buffer.from(target);
621
+ const nativeBytes = Buffer.isBuffer(target) ? target : Buffer.from(target);
622
+ // Windows readlink may spell a relative Git target with native separators.
623
+ const bytes = process.platform === "win32" ? Buffer.from(nativeBytes.toString("utf8").replaceAll("\\", "/")) : nativeBytes;
597
624
  assertSafeSymlinkTarget(root, entry.path, bytes);
598
625
  return createHash("sha256").update(bytes).digest("hex");
599
626
  }
@@ -638,7 +665,13 @@ function candidateOwnerPreparationError(error: unknown): CandidateViewError {
638
665
  // Surface the bounded errno of the underlying failure directly in the
639
666
  // message so CI logs are self-diagnosing without TAP printing the cause.
640
667
  const code = (error as NodeJS.ErrnoException | undefined)?.code;
641
- return new CandidateViewError(`candidate view owner preparation failed${typeof code === "string" && code ? ` (${code})` : ""}`, "candidate-owner-preparation-failed", undefined, { cause: error });
668
+ const safeCode = typeof code === "string" && /^(?:EACCES|EPERM|ENOENT|EIO|ETIMEDOUT)$/.test(code) ? code : undefined;
669
+ const diagnostics = error instanceof PosixCandidateOwnerParentPrivacyError
670
+ ? { code: "candidate-owner-parent-privacy" as const, message: CANDIDATE_OWNER_PARENT_PRIVACY_MESSAGE }
671
+ : error instanceof PosixCandidateOwnerParentChmodIneffectiveError
672
+ ? { code: "candidate-owner-parent-chmod-ineffective" as const, message: CANDIDATE_OWNER_PARENT_CHMOD_INEFFECTIVE_MESSAGE }
673
+ : undefined;
674
+ return new CandidateViewError(`candidate view owner preparation failed${safeCode ? ` (${safeCode})` : ""}`, "candidate-owner-preparation-failed", diagnostics, { cause: error });
642
675
  }
643
676
 
644
677
  function candidateViewParent(commonDir: string, platform: NodeJS.Platform): string {
@@ -789,6 +822,36 @@ export function resolveCanonicalCandidateBase(contributorRoot: string, baseRef:
789
822
  return resolveCandidateBase(realpathSync(contributorRoot), baseRef, process.env, defaultCandidateGitExecutor);
790
823
  }
791
824
 
825
+ /** Classifies a full provider-owned object id without resolving it as a caller commit/ref. */
826
+ export function isProviderCandidateBaseTree(contributorRoot: string, objectId: string): boolean {
827
+ if (!isFullCommitId(objectId)) return false;
828
+ const cwd = realpathSync(contributorRoot);
829
+ const probe = probeCandidateGit(cwd, ["cat-file", "-t", objectId], process.env, defaultCandidateGitExecutor);
830
+ return probe.status === 0 && probe.stdout === "tree";
831
+ }
832
+
833
+ function materializeSymlinkEntries(root: string, entries: readonly CandidateTreeEntry[], executor: CandidateGitExecutor): ReadonlyMap<string, string> {
834
+ // Validate every original blob before any symlink is created. In particular,
835
+ // never use Git's core.symlinks-dependent regular-file checkout for mode 120000.
836
+ const links = entries.filter((entry) => entry.mode === "120000").map((entry) => {
837
+ const bytes = candidateGit(root, ["cat-file", "blob", entry.objectId], process.env, "buffer", executor) as Buffer;
838
+ assertSafeSymlinkTarget(root, entry.path, bytes);
839
+ return { path: entry.path, target: bytes.toString("utf8") };
840
+ });
841
+ for (const link of links) {
842
+ const path = join(root, link.path);
843
+ mkdirSync(dirname(path), { recursive: true });
844
+ try {
845
+ const targetPath = resolve(dirname(path), link.target);
846
+ const targetType = process.platform === "win32" && lstatSync(targetPath, { throwIfNoEntry: false })?.isDirectory() ? "dir" : "file";
847
+ symlinkSync(link.target, path, targetType);
848
+ } catch (error) {
849
+ throw new CandidateViewError("candidate view cannot create native symlinks; enable symlink capability out of band before retrying START", "symlink-materialization-failed", undefined, { cause: error });
850
+ }
851
+ }
852
+ return new Map(links.map((link) => [link.path, createHash("sha256").update(Buffer.from(link.target, "utf8")).digest("hex")]));
853
+ }
854
+
792
855
  function checkoutMaterializedEntries(root: string, entries: readonly CandidateTreeEntry[], executor: CandidateGitExecutor): void {
793
856
  let batch: string[] = [];
794
857
  let bytes = 0;
@@ -798,6 +861,7 @@ function checkoutMaterializedEntries(root: string, entries: readonly CandidateTr
798
861
  batch = []; bytes = 0;
799
862
  };
800
863
  for (const entry of entries) {
864
+ if (entry.mode === "120000") continue;
801
865
  const size = Buffer.byteLength(entry.path, "utf8") + 1;
802
866
  if (batch.length > 0 && bytes + size > 16_384) flush();
803
867
  batch.push(entry.path); bytes += size;
@@ -885,10 +949,16 @@ function seedPrivateIndexFromLiveIndex(cwd: string, indexPath: string, executor:
885
949
  function materializeCandidateView(request: CreateCandidateViewRequest, executor: CandidateGitExecutor, platform: NodeJS.Platform = process.platform): CandidateViewRecord {
886
950
  const contributorRoot = realpathSync(request.contributorRoot);
887
951
  if (!lstatSync(contributorRoot).isDirectory()) throw new CandidateViewError("contributor root is not a directory");
888
- if (request.committedOnly === true && request.baseRef === undefined) throw new CandidateViewError("committed-only candidate views require an explicit base reference", "committed-only-base-required");
952
+ if (request.committedOnly === true && request.baseRef === undefined && request.providerBaseTree === undefined) throw new CandidateViewError("committed-only candidate views require an explicit base reference", "committed-only-base-required");
953
+ if (request.providerBaseTree !== undefined && (request.baseRef !== undefined || request.committedOnly !== true || !isFullCommitId(request.providerBaseTree))) throw new CandidateViewError("provider candidate base tree is invalid", "provider-base-tree-invalid");
889
954
  const commonDir = resolve(contributorRoot, git(contributorRoot, ["rev-parse", "--git-common-dir"], process.env, executor));
890
955
  const canonicalCommonDir = realpathSync(commonDir);
891
- const base = resolveCandidateBase(contributorRoot, request.baseRef, process.env, executor);
956
+ // A provider tree is a diff base, not a commit selector. Anchor the private
957
+ // worktree to HEAD without searching for a commit with a matching tree.
958
+ const anchor = resolveCandidateBase(contributorRoot, request.providerBaseTree === undefined ? request.baseRef : "HEAD", process.env, executor);
959
+ const providerBaseTree = request.providerBaseTree;
960
+ if (providerBaseTree !== undefined && (git(contributorRoot, ["cat-file", "-t", providerBaseTree], process.env, executor) !== "tree" || providerBaseTree.length !== anchor.tree.length)) throw new CandidateViewError("provider candidate base is not a tree", "provider-base-tree-invalid");
961
+ const base = providerBaseTree === undefined ? anchor : { commit: anchor.commit, tree: providerBaseTree };
892
962
  const committedOnly = request.committedOnly === true;
893
963
  const intendedUntracked = normalizeIntendedUntracked(request.intendedUntracked);
894
964
  const candidateCommit = committedOnly
@@ -941,11 +1011,16 @@ function materializeCandidateView(request: CreateCandidateViewRequest, executor:
941
1011
  git(root, ["read-tree", candidateTree], process.env, executor);
942
1012
  const tree = parseTree(root, candidateTree, executor);
943
1013
  checkoutMaterializedEntries(root, tree.entries, executor);
944
- const entries = tree.entries.map((entry) => ({ ...entry, contentHash: entryContentHash(root, entry) }));
1014
+ const symlinkHashes = materializeSymlinkEntries(root, tree.entries, executor);
1015
+ const entries = tree.entries.map((entry) => {
1016
+ const contentHash = entryContentHash(root, entry);
1017
+ if (entry.mode === "120000" && contentHash !== symlinkHashes.get(entry.path)) throw new CandidateViewError("candidate view symlink does not match its frozen blob");
1018
+ return { ...entry, contentHash };
1019
+ });
945
1020
  const scope = deriveChangedScope(contributorRoot, base.tree, candidateTree, [...tree.entries, ...tree.gitlinks], executor);
946
1021
  for (const gitlink of tree.gitlinks) if (lstatSync(join(root, gitlink.path), { throwIfNoEntry: false })) throw new CandidateViewError("candidate view materialized a metadata-only gitlink");
947
1022
  makeReadonly(root, entries);
948
- return { owner, token: basename(root), root: realpathSync(root), parent, contributorRoot, commonDir: canonicalCommonDir, baseCommit, baseTree: base.tree, candidateTree, committedOnly, intendedUntracked, entries, gitlinks: tree.gitlinks, scope, gitExecutor: executor };
1023
+ return { owner, token: basename(root), root: realpathSync(root), parent, contributorRoot, commonDir: canonicalCommonDir, baseCommit, baseTree: base.tree, ...(providerBaseTree === undefined ? {} : { providerBaseTree }), candidateTree, committedOnly, intendedUntracked, entries, gitlinks: tree.gitlinks, scope, gitExecutor: executor };
949
1024
  } catch (error) {
950
1025
  try { removeCandidateOwner(owner, (args) => git(canonicalCommonDir, args, process.env, executor), makeWritableForCleanup, false, platform); } catch { /* Preserve the marker and any partial worktree for conservative recovery. */ }
951
1026
  throw error;
@@ -1192,7 +1267,8 @@ export class CandidateViewRegistry {
1192
1267
  if (this.current.has(root)) throw new CandidateViewError("candidate view already has a current lineage binding", "current-binding-already-established");
1193
1268
  if (states.length === 0) throw new CandidateViewError("no authoritative reviewing lineage exactly matches the live candidate", "authoritative-current-match-missing");
1194
1269
  if (states.length !== 1) throw new CandidateViewError("multiple authoritative reviewing lineages exactly match the live candidate", "authoritative-current-match-ambiguous");
1195
- const live = materializeCandidateView({ contributorRoot: root, baseRef: states[0]!.baseCommit, committedOnly: states[0]!.committedOnly === true, ...(states[0]!.intendedUntracked === undefined ? {} : { intendedUntracked: states[0]!.intendedUntracked }) }, this.gitExecutor, this.platform);
1270
+ const state = states[0]!;
1271
+ const live = materializeCandidateView({ contributorRoot: root, ...(state.providerBaseTree === undefined ? { baseRef: state.baseCommit } : { providerBaseTree: state.providerBaseTree }), committedOnly: state.committedOnly === true, ...(state.intendedUntracked === undefined ? {} : { intendedUntracked: state.intendedUntracked }) }, this.gitExecutor, this.platform);
1196
1272
  try {
1197
1273
  const matches = states.filter((state) => this.matchesAuthoritativeState(live, state));
1198
1274
  if (matches.length === 0) throw new CandidateViewError("no authoritative reviewing lineage exactly matches the live candidate", "authoritative-current-match-missing");
@@ -1220,7 +1296,7 @@ export class CandidateViewRegistry {
1220
1296
  assertRecordSafe(existing, this.platform);
1221
1297
  return this.expose(existing);
1222
1298
  }
1223
- const record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(projection.intendedUntracked === undefined ? {} : { intendedUntracked: projection.intendedUntracked }) }, this.gitExecutor, this.platform);
1299
+ const record = materializeCandidateView({ contributorRoot: root, ...(projection.providerBaseTree === undefined ? { baseRef: projection.baseCommit } : { providerBaseTree: projection.providerBaseTree }), committedOnly: projection.committedOnly, ...(projection.intendedUntracked === undefined ? {} : { intendedUntracked: projection.intendedUntracked }) }, this.gitExecutor, this.platform);
1224
1300
  try {
1225
1301
  if (record.baseCommit !== projection.baseCommit || record.baseTree !== projection.baseTree) throw new CandidateViewError("corrected candidate base does not match the frozen genesis base");
1226
1302
  if (!record.scope.paths.every((path) => projection.paths.includes(path))) throw new CandidateViewError("corrected candidate scope escapes the frozen genesis paths");
@@ -1253,6 +1329,7 @@ export class CandidateViewRegistry {
1253
1329
  replacement.contributorRoot !== projection.contributorRoot ||
1254
1330
  replacement.baseCommit !== projection.baseCommit ||
1255
1331
  replacement.baseTree !== projection.baseTree ||
1332
+ replacement.providerBaseTree !== projection.providerBaseTree ||
1256
1333
  replacement.committedOnly !== projection.committedOnly ||
1257
1334
  JSON.stringify(replacement.intendedUntracked ?? null) !== JSON.stringify(projection.intendedUntracked ?? null) ||
1258
1335
  !replacement.scope.paths.every((path) => projection.paths.includes(path))
@@ -1267,6 +1344,7 @@ export class CandidateViewRegistry {
1267
1344
  contributorRoot: replacement.contributorRoot,
1268
1345
  baseCommit: replacement.baseCommit,
1269
1346
  baseTree: replacement.baseTree,
1347
+ ...(replacement.providerBaseTree === undefined ? {} : { providerBaseTree: replacement.providerBaseTree }),
1270
1348
  candidateTree: replacement.candidateTree,
1271
1349
  committedOnly: replacement.committedOnly,
1272
1350
  intendedUntracked: replacement.intendedUntracked,
@@ -1293,6 +1371,7 @@ export class CandidateViewRegistry {
1293
1371
  return realpathSync(state.contributorRoot) === record.contributorRoot &&
1294
1372
  state.baseCommit === record.baseCommit &&
1295
1373
  state.baseTree === record.baseTree &&
1374
+ state.providerBaseTree === record.providerBaseTree &&
1296
1375
  state.candidateTree === record.candidateTree &&
1297
1376
  (state.committedOnly ?? false) === record.committedOnly &&
1298
1377
  (state.intendedUntracked === undefined || JSON.stringify(state.intendedUntracked) === JSON.stringify(record.intendedUntracked)) &&
@@ -1317,6 +1396,7 @@ export class CandidateViewRegistry {
1317
1396
  contributorRoot: record.contributorRoot,
1318
1397
  baseCommit: record.baseCommit,
1319
1398
  baseTree: record.baseTree,
1399
+ ...(record.providerBaseTree === undefined ? {} : { providerBaseTree: record.providerBaseTree }),
1320
1400
  candidateTree: record.candidateTree,
1321
1401
  committedOnly: record.committedOnly,
1322
1402
  intendedUntracked: record.intendedUntracked,
@@ -1341,15 +1421,17 @@ export class CandidateViewRegistry {
1341
1421
  this.projections.set(key, { contributorRoot: root, baseCommit, baseTree, candidateTree, committedOnly: false, paths: [...paths], modes: {}, gitlinks: {}, deletedPaths: [] });
1342
1422
  }
1343
1423
 
1344
- restoreProjectionFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor): void {
1424
+ restoreProjectionFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor, providerBaseTree?: string): void {
1345
1425
  const root = this.canonicalRoot(contributorRoot);
1346
1426
  const key = this.lineageKey(root, lineageId);
1347
1427
  if (!lineageId || this.projections.has(key) || !isFullCommitId(descriptor.baseTree) || !isFullCommitId(descriptor.currentCandidateTree)) throw new CandidateViewError("native frozen projection is invalid or already restored");
1348
1428
  if (descriptor.paths.some((path) => !isSafeCandidatePath(path)) || new Set(descriptor.paths).size !== descriptor.paths.length) throw new CandidateViewError("native frozen projection paths are invalid");
1349
1429
  if (descriptor.intendedUntracked.some((path) => !descriptor.paths.includes(path)) || new Set(descriptor.intendedUntracked).size !== descriptor.intendedUntracked.length) throw new CandidateViewError("native intended-untracked projection is invalid");
1350
1430
  const head = resolveCandidateBase(root, "HEAD", process.env, this.gitExecutor);
1351
- const base = head.tree === descriptor.baseTree ? head : resolveCandidateBaseTree(root, descriptor.baseTree, this.gitExecutor);
1431
+ if (providerBaseTree !== undefined && providerBaseTree !== descriptor.baseTree) throw new CandidateViewError("provider base tree does not match the native frozen projection", "provider-base-tree-invalid");
1432
+ const base = head.tree === descriptor.baseTree ? head : providerBaseTree === undefined ? resolveCandidateBaseTree(root, descriptor.baseTree, this.gitExecutor) : { commit: head.commit, tree: providerBaseTree };
1352
1433
  const committedOnly = head.tree === descriptor.currentCandidateTree && base.tree !== head.tree;
1434
+ if (providerBaseTree !== undefined && !committedOnly) throw new CandidateViewError("provider base tree requires a committed frozen projection", "provider-base-tree-invalid");
1353
1435
  if (!committedOnly && head.tree !== descriptor.baseTree) throw new CandidateViewError("native projection base no longer matches HEAD");
1354
1436
  // Native `staged` covers both a committed HEAD range and the exact current
1355
1437
  // index over HEAD. Re-derive the latter from Git instead of trusting the
@@ -1377,6 +1459,7 @@ export class CandidateViewRegistry {
1377
1459
  contributorRoot: root,
1378
1460
  baseCommit: base.commit,
1379
1461
  baseTree: descriptor.baseTree,
1462
+ ...(providerBaseTree === undefined ? {} : { providerBaseTree }),
1380
1463
  candidateTree: descriptor.currentCandidateTree,
1381
1464
  committedOnly,
1382
1465
  intendedUntracked: Object.freeze([...descriptor.intendedUntracked]),
@@ -1407,15 +1490,15 @@ export class CandidateViewRegistry {
1407
1490
  * here on purpose — the lenses that consumed it finished before the
1408
1491
  * correction.
1409
1492
  */
1410
- rebindForFinalizeFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor): CandidateView {
1411
- return this.restoreForFinalizeFromNative(lineageId, contributorRoot, descriptor);
1493
+ rebindForFinalizeFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor, providerBaseTree?: string): CandidateView {
1494
+ return this.restoreForFinalizeFromNative(lineageId, contributorRoot, descriptor, providerBaseTree);
1412
1495
  }
1413
1496
 
1414
1497
  /**
1415
1498
  * Restores a lineage's FINALIZE binding (gentle-pi #185): the stale entry
1416
1499
  * is only detached, not destroyed, so a failed restore can undo it.
1417
1500
  */
1418
- restoreForFinalizeFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor): CandidateView {
1501
+ restoreForFinalizeFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor, providerBaseTree?: string): CandidateView {
1419
1502
  const root = this.canonicalRoot(contributorRoot);
1420
1503
  const key = this.lineageKey(root, lineageId);
1421
1504
  const staleToken = this.lineages.get(key), staleProjection = this.projections.get(key);
@@ -1423,16 +1506,16 @@ export class CandidateViewRegistry {
1423
1506
  let projectionRestored = false;
1424
1507
  let record: CandidateViewRecord | undefined;
1425
1508
  try {
1426
- this.restoreProjectionFromNative(lineageId, root, descriptor);
1509
+ this.restoreProjectionFromNative(lineageId, root, descriptor, providerBaseTree);
1427
1510
  projectionRestored = true;
1428
1511
  const projection = this.resolveProjection(lineageId, root);
1429
1512
  const matchesProjection = (candidate: CandidateViewRecord): boolean => candidate.baseTree === projection.baseTree && candidate.candidateTree === projection.candidateTree && JSON.stringify(candidate.scope.paths) === JSON.stringify(projection.paths);
1430
1513
  const emptyIntendedUntracked = projection.intendedUntracked?.length === 0;
1431
- record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(!emptyIntendedUntracked && projection.intendedUntracked !== undefined ? { intendedUntracked: projection.intendedUntracked } : {}) }, this.gitExecutor, this.platform);
1514
+ record = materializeCandidateView({ contributorRoot: root, ...(projection.providerBaseTree === undefined ? { baseRef: projection.baseCommit } : { providerBaseTree: projection.providerBaseTree }), committedOnly: projection.committedOnly, ...(!emptyIntendedUntracked && projection.intendedUntracked !== undefined ? { intendedUntracked: projection.intendedUntracked } : {}) }, this.gitExecutor, this.platform);
1432
1515
  if (!matchesProjection(record) && emptyIntendedUntracked) {
1433
1516
  this.remove(record);
1434
1517
  record = undefined;
1435
- record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, intendedUntracked: [] }, this.gitExecutor, this.platform);
1518
+ record = materializeCandidateView({ contributorRoot: root, ...(projection.providerBaseTree === undefined ? { baseRef: projection.baseCommit } : { providerBaseTree: projection.providerBaseTree }), committedOnly: projection.committedOnly, intendedUntracked: [] }, this.gitExecutor, this.platform);
1436
1519
  }
1437
1520
  if (!matchesProjection(record)) throw new CandidateViewError("live candidate does not match the native frozen projection");
1438
1521
  this.records.set(record.token, record);
@@ -1461,7 +1544,7 @@ export class CandidateViewRegistry {
1461
1544
  * dispatch-facing current binding is established with the provider-named
1462
1545
  * pending lenses.
1463
1546
  */
1464
- restoreCurrentForDispatchFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor, selectedLenses: readonly string[]): void {
1547
+ restoreCurrentForDispatchFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor, selectedLenses: readonly string[], providerBaseTree?: string): void {
1465
1548
  const root = this.canonicalRoot(contributorRoot);
1466
1549
  const key = this.lineageKey(root, lineageId);
1467
1550
  if (this.current.has(root)) throw new CandidateViewError("candidate view already has a current lineage binding", "current-binding-already-established");
@@ -1469,10 +1552,13 @@ export class CandidateViewRegistry {
1469
1552
  let record: CandidateViewRecord | undefined;
1470
1553
  try {
1471
1554
  const lenses = this.validateSelectedLenses(selectedLenses);
1472
- this.restoreProjectionFromNative(lineageId, root, descriptor);
1555
+ // A fresh STATUS descriptor alone cannot assert provider-tree provenance.
1556
+ // Without a separately authenticated offer, restoreProjectionFromNative
1557
+ // resolves only its existing commit-backed or live-HEAD path.
1558
+ this.restoreProjectionFromNative(lineageId, root, descriptor, providerBaseTree);
1473
1559
  projectionRestored = true;
1474
1560
  const projection = this.resolveProjection(lineageId, root);
1475
- record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(projection.intendedUntracked === undefined ? {} : { intendedUntracked: projection.intendedUntracked }) }, this.gitExecutor, this.platform);
1561
+ record = materializeCandidateView({ contributorRoot: root, ...(projection.providerBaseTree === undefined ? { baseRef: projection.baseCommit } : { providerBaseTree: projection.providerBaseTree }), committedOnly: projection.committedOnly, ...(projection.intendedUntracked === undefined ? {} : { intendedUntracked: projection.intendedUntracked }) }, this.gitExecutor, this.platform);
1476
1562
  if (record.baseTree !== projection.baseTree || record.candidateTree !== projection.candidateTree || JSON.stringify(record.scope.paths) !== JSON.stringify(projection.paths)) {
1477
1563
  throw new CandidateViewError("live candidate does not match the native frozen projection");
1478
1564
  }
@@ -1566,7 +1652,7 @@ export class CandidateViewRegistry {
1566
1652
  }
1567
1653
 
1568
1654
  private assertCurrentBindingMatchesLiveCandidate(record: CandidateViewRecord): void {
1569
- const live = materializeCandidateView({ contributorRoot: record.contributorRoot, baseRef: record.baseCommit, committedOnly: record.committedOnly, ...(record.intendedUntracked === undefined ? {} : { intendedUntracked: record.intendedUntracked }) }, this.gitExecutor, this.platform);
1655
+ const live = materializeCandidateView({ contributorRoot: record.contributorRoot, ...(record.providerBaseTree === undefined ? { baseRef: record.baseCommit } : { providerBaseTree: record.providerBaseTree }), committedOnly: record.committedOnly, ...(record.intendedUntracked === undefined ? {} : { intendedUntracked: record.intendedUntracked }) }, this.gitExecutor, this.platform);
1570
1656
  try {
1571
1657
  if (
1572
1658
  live.baseCommit !== record.baseCommit ||
@@ -1634,6 +1720,7 @@ export class CandidateViewRegistry {
1634
1720
  contributorRoot: record.contributorRoot,
1635
1721
  baseCommit: record.baseCommit,
1636
1722
  baseTree: record.baseTree,
1723
+ ...(record.providerBaseTree === undefined ? {} : { providerBaseTree: record.providerBaseTree }),
1637
1724
  candidateTree: record.candidateTree,
1638
1725
  committedOnly: record.committedOnly,
1639
1726
  intendedUntracked: record.intendedUntracked,
@@ -1774,7 +1861,7 @@ export function decodeCandidateContextManifest(encoded: string, sha256: string):
1774
1861
  if (actualSha256 !== sha256) throw new CandidateViewError("candidate context manifest integrity check failed", "candidate-context-manifest-integrity");
1775
1862
  const text = bytes.toString("utf8");
1776
1863
  if (!Buffer.from(text, "utf8").equals(bytes)) return invalidCandidateContextManifest("candidate context manifest is not valid UTF-8");
1777
- if (gzipSync(bytes, { mtime: 0 }).toString("base64url") !== encoded) {
1864
+ if (gzipSync(bytes).toString("base64url") !== encoded) {
1778
1865
  return invalidCandidateContextManifest("candidate context manifest transport is not canonical");
1779
1866
  }
1780
1867
  let value: unknown;
@@ -1836,7 +1923,8 @@ function compactCandidateContextBlock(lineageId: string, agents: readonly Review
1836
1923
  const bytes = Buffer.from(JSON.stringify(manifest), "utf8");
1837
1924
  if (bytes.length > MAX_CANDIDATE_CONTEXT_MANIFEST_BYTES) throw new CandidateViewError("candidate view context exceeds the bounded dispatch contract");
1838
1925
  const sha256 = createHash("sha256").update(bytes).digest("hex");
1839
- const encoded = gzipSync(bytes, { mtime: 0 }).toString("base64url");
1926
+ // Node emits a zero gzip timestamp by default; no unsupported mtime option is needed.
1927
+ const encoded = gzipSync(bytes).toString("base64url");
1840
1928
  const block = `${candidateContextPreamble(lineageId, agents, view, scopeSemantics)}\nFrozen changed scope manifest (gzip+base64url): \`${encoded}\`.\nFrozen changed scope manifest SHA-256: \`${sha256}\`.\nCall \`gentle_review_scope\` with exactly this manifest, SHA-256, and cursor 0; continue with each returned \`nextCursor\` until absent. It is the only authorized scope enumerator: do not infer scope by traversing the candidate or ambient tree. Gitlinks are metadata-only and MUST NOT be traversed.\nThe ambient contributor working directory is out of scope. This controller-owned context is immutable; you are read-only and your output is untrusted.`;
1841
1929
  if (Buffer.byteLength(block, "utf8") > MAX_CANDIDATE_CONTEXT_LENGTH) throw new CandidateViewError("candidate view context exceeds the bounded dispatch contract");
1842
1930
  return block;
@@ -136,7 +136,7 @@ function parseCompactFinalizeInputValue(value: unknown): CompactFinalizeContract
136
136
  if ((input.final_evidence === undefined && outcomeFields !== 0) || (input.final_evidence !== undefined && outcomeFields !== 1)) fail("review/finalize", "field-pair", "final evidence requires exactly one verification result or outcome");
137
137
  let correction_line_forecast: number | undefined;
138
138
  if (input.correction_line_forecast !== undefined) {
139
- if (!Number.isSafeInteger(input.correction_line_forecast) || input.correction_line_forecast <= 0) fail("review/finalize.correction_line_forecast", "range", "must be a positive safe integer");
139
+ if (typeof input.correction_line_forecast !== "number" || !Number.isSafeInteger(input.correction_line_forecast) || input.correction_line_forecast <= 0) fail("review/finalize.correction_line_forecast", "range", "must be a positive safe integer");
140
140
  correction_line_forecast = input.correction_line_forecast;
141
141
  }
142
142
  if (input.final_verification_passed !== undefined && typeof input.final_verification_passed !== "boolean") fail("review/finalize.final_verification_passed", "type", "must be boolean");
@@ -154,7 +154,7 @@ function parseCompactFinalizeInputValue(value: unknown): CompactFinalizeContract
154
154
  if (typeof input.final_evidence !== "string" || input.final_evidence.length === 0) fail("review/finalize.final_evidence", "empty", "must contain at least one byte");
155
155
  final_evidence = input.final_evidence;
156
156
  }
157
- return { cwd: string(input.cwd, "review/finalize.cwd"), ...(optionalLineage(input.lineageId, "review/finalize.lineageId") === undefined ? {} : { lineageId: optionalLineage(input.lineageId, "review/finalize.lineageId")! }), ...(correction_line_forecast === undefined ? {} : { correction_line_forecast }), ...(input.validation === undefined ? {} : { validation: parseValidation(input.validation, "review/finalize.validation") }), ...(final_evidence === undefined ? {} : { final_evidence }), ...(input.final_verification_passed === undefined ? {} : { final_verification_passed: input.final_verification_passed }), ...(final_verification_outcome === undefined ? {} : { final_verification_outcome }), ...(input.reviewer_run_acknowledged === undefined ? {} : { reviewer_run_acknowledged: input.reviewer_run_acknowledged as boolean }) };
157
+ return { cwd: string(input.cwd, "review/finalize.cwd"), ...(optionalLineage(input.lineageId, "review/finalize.lineageId") === undefined ? {} : { lineageId: optionalLineage(input.lineageId, "review/finalize.lineageId")! }), ...(correction_line_forecast === undefined ? {} : { correction_line_forecast }), ...(input.validation === undefined ? {} : { validation: parseValidation(input.validation, "review/finalize.validation") }), ...(final_evidence === undefined ? {} : { final_evidence }), ...(typeof input.final_verification_passed !== "boolean" ? {} : { final_verification_passed: input.final_verification_passed }), ...(final_verification_outcome === undefined ? {} : { final_verification_outcome }), ...(typeof input.reviewer_run_acknowledged !== "boolean" ? {} : { reviewer_run_acknowledged: input.reviewer_run_acknowledged }) };
158
158
  }
159
159
 
160
160
  export function parseNativeCompactFinalizeInput(value: unknown): CompactFinalizeContractInput {
@@ -34,13 +34,14 @@ import { spawn } from "node:child_process";
34
34
  import { tmpdir } from "node:os";
35
35
  import { isAbsolute, join } from "node:path";
36
36
  import { completeSimple } from "@earendil-works/pi-ai/compat";
37
- import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
37
+ import { gentleAiIsolationEnv, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
38
38
  import {
39
39
  INPROCESS_REVIEWER_FAILURE,
40
40
  runInProcessReviewer,
41
41
  type InProcessReviewerFailureCode,
42
42
  type InProcessReviewerOutcome,
43
43
  type InProcessReviewerRegistry,
44
+ type InProcessReviewerRequest,
44
45
  } from "./inprocess-reviewer.ts";
45
46
  import { REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION, REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3 } from "./review-integration-v2.ts";
46
47
  import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
@@ -386,6 +387,8 @@ export interface ReviewHostRelayRequest {
386
387
  readonly selection?: string;
387
388
  /** The routing entry's thinking label, forwarded verbatim to the completion. */
388
389
  readonly thinking?: string;
390
+ /** Effective Pi transport preference, forwarded without forcing a provider default. */
391
+ readonly reviewerTransport?: InProcessReviewerRequest["transport"];
389
392
  /**
390
393
  * The caller's live pi session id, forwarded into the in-process completion
391
394
  * request so an OpenCode-routed reviewer model carries its
@@ -498,7 +501,8 @@ function collectGentleAiProcess(
498
501
  const startedAt = Date.now();
499
502
  const child = spawn(file, [...arguments_], {
500
503
  cwd: options.cwd,
501
- env: options.env,
504
+ // S9: a bundled install keeps Gentle AI's state inside its prefix.
505
+ env: gentleAiIsolationEnv(options.env),
502
506
  stdio: ["pipe", "pipe", "pipe"],
503
507
  shell: false,
504
508
  windowsHide: true,
@@ -737,6 +741,7 @@ export async function prepareReviewHostRelaySlot(
737
741
  {
738
742
  selection: preparedRequest.selection!,
739
743
  ...(preparedRequest.thinking === undefined ? {} : { thinking: preparedRequest.thinking }),
744
+ ...(preparedRequest.reviewerTransport === undefined ? {} : { transport: preparedRequest.reviewerTransport }),
740
745
  ...(preparedRequest.reviewerSessionId === undefined ? {} : { sessionId: preparedRequest.reviewerSessionId }),
741
746
  prompt: promptBytes,
742
747
  timeoutMs: piTimeoutMs,