gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -7,7 +7,18 @@ import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import test, { after, before } from "node:test";
9
9
  import type { ExtensionAPI, ExtensionContext, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
10
- import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
10
+ import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
11
+
12
+ // S5 (startup-home-repo-and-changes): startup review negotiation runs after
13
+ // session_start returns and passes its deadline signal to native STATUS.
14
+ // These tests observe negotiation outcomes, so they wait for it, and they
15
+ // compare request contracts without the per-call signal.
16
+ const settleStartup = () => __testing.startupNegotiationSettled();
17
+ function withoutSignal<T>(request: T): T {
18
+ if (request === null || typeof request !== "object" || !("signal" in request)) return request;
19
+ const { signal: _signal, ...rest } = request as Record<string, unknown>;
20
+ return rest as T;
21
+ }
11
22
  import { NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError, type NativeReviewCli } from "../lib/native-review-cli.ts";
12
23
  import type { ReviewStatusV3 } from "../lib/review-integration-v2.ts";
13
24
 
@@ -235,7 +246,7 @@ for (const scenario of ["same", "changed", "sibling-root", "nested-root", "faile
235
246
  const native = {
236
247
  reviewMode: onMode("on"),
237
248
  targetStatus: async (request: unknown) => {
238
- statusRequests.push(request);
249
+ statusRequests.push(withoutSignal(request));
239
250
  return burned ? executeStartStatus(nextTarget) : approved;
240
251
  },
241
252
  acknowledgeApproved: async (request: unknown) => {
@@ -260,6 +271,7 @@ for (const scenario of ["same", "changed", "sibling-root", "nested-root", "faile
260
271
  await directWrite(handlers, session);
261
272
  const result = await review.execute("post-ack", { operation: "acknowledge-approved", lineageId }, undefined, undefined, toolContext(session));
262
273
  if (unsuccessful) {
274
+ assert.ok(typeof result.details === "object" && result.details !== null && "outcome" in result.details && "mutation_outcome" in result.details);
263
275
  assert.equal(result.details.outcome, scenario === "failed" ? "native-operation-failed" : "native-mutation-status-reconciled");
264
276
  assert.equal(result.details.mutation_outcome, scenario === "failed" ? "none" : "unknown");
265
277
  } else assert.deepEqual(result.details, {
@@ -300,6 +312,7 @@ test("passive session events outside Git with RDD off never invoke native STATUS
300
312
  const { handlers } = harness(native);
301
313
  const session = ctx("outside-git", true, cwd);
302
314
  await handlers.get("session_start")!({ type: "session_start" }, session);
315
+ await settleStartup();
303
316
  await handlers.get("agent_end")!(agentEndEvent, session);
304
317
  assert.deepEqual(calls, [], "passive non-Git events need no native entry");
305
318
  assert.equal(existsSync(join(cwd, ".git")), false, "passive events must not initialize Git");
@@ -311,7 +324,7 @@ test("agent_end performs no STATUS call and sends nothing when RDD is off", asyn
311
324
  const native = {
312
325
  reviewMode: onMode("off"),
313
326
  targetStatus: async (request: unknown) => {
314
- statusRequests.push(request);
327
+ statusRequests.push(withoutSignal(request));
315
328
  throw new Error("targetStatus must not be called when RDD is off");
316
329
  },
317
330
  } as unknown as NativeReviewCli;
@@ -329,7 +342,7 @@ test("agent_end nudges exactly once when RDD is on and STATUS offers review.star
329
342
  const native = {
330
343
  reviewMode: onMode("on"),
331
344
  targetStatus: async (request: { agent?: string }) => {
332
- statusRequests.push(request);
345
+ statusRequests.push(withoutSignal(request));
333
346
  return executeStartStatus(targetIdentity);
334
347
  },
335
348
  } as unknown as NativeReviewCli;
@@ -351,6 +364,30 @@ test("agent_end nudges exactly once when RDD is on and STATUS offers review.star
351
364
  assert.equal(statusRequests[0]?.agent, "pi");
352
365
  });
353
366
 
367
+ // verify-always-rdd-high S2: automatic review is for high risk only. When the
368
+ // native assessment reports review_due false (a medium or passive candidate),
369
+ // agent_end never nudges; review_due true still nudges. An assessment that
370
+ // cannot answer keeps the nudge, so a reminder is never lost to an error.
371
+ test("agent_end nudges only when the native assessment reports review_due", async () => {
372
+ const targetIdentity = `sha256:${"b".repeat(64)}`;
373
+ for (const [label, assess, wantNudges] of [
374
+ ["medium, not due", async () => ({ risk: "medium", reviewDue: false, reviewDueReason: "under_budget" }), 0],
375
+ ["high, due", async () => ({ risk: "high", reviewDue: true, reviewDueReason: "high_risk" }), 1],
376
+ ["assessment rejects", async () => { throw new Error("assess failed"); }, 1],
377
+ ] as const) {
378
+ const native = {
379
+ reviewMode: onMode("on"),
380
+ targetStatus: async () => executeStartStatus(targetIdentity),
381
+ assess,
382
+ } as unknown as NativeReviewCli;
383
+ const { handlers, sent } = harness(native);
384
+ const session = ctx(`agent-end-review-due-${label.replace(/\W+/g, "-")}`);
385
+ await directWrite(handlers, session);
386
+ await handlers.get("agent_end")!(agentEndEvent, session);
387
+ assert.equal(sent.length, wantNudges, label);
388
+ }
389
+ });
390
+
354
391
  test("agent_end nudges once per target identity and again for a fresh identity", async () => {
355
392
  let targetIdentity = `sha256:${"b".repeat(64)}`;
356
393
  const native = {
@@ -431,7 +468,7 @@ test("gentle-ai-worker agent_end never queries native review", async () => {
431
468
  const native = {
432
469
  reviewMode: onMode("on"),
433
470
  targetStatus: async (request: unknown) => {
434
- statusRequests.push(request);
471
+ statusRequests.push(withoutSignal(request));
435
472
  throw new Error("a bounded worker must not own native review");
436
473
  },
437
474
  } as unknown as NativeReviewCli;
@@ -452,7 +489,7 @@ test("agent_end sends nothing and performs no STATUS call when GENTLE_PI_AGENTS_
452
489
  const native = {
453
490
  reviewMode: onMode("on"),
454
491
  targetStatus: async (request: unknown) => {
455
- statusRequests.push(request);
492
+ statusRequests.push(withoutSignal(request));
456
493
  throw new Error("targetStatus must not be called in a child session");
457
494
  },
458
495
  } as unknown as NativeReviewCli;
@@ -538,12 +575,14 @@ for (const scenario of ["reload", "fork", "new", "off-branch"] as const) {
538
575
  const first = harness(native, entries);
539
576
  const session = ctx(`receipt-${scenario}`, true, cwd);
540
577
  await first.handlers.get("session_start")!({}, session);
578
+ await settleStartup();
541
579
  await directWrite(first.handlers, session);
542
580
  await first.handlers.get("session_shutdown")!({ reason: "reload" }, session);
543
581
  const resumed = harness(native, entries);
544
582
  const next = ctx(scenario === "reload" || scenario === "off-branch" ? `receipt-${scenario}` : `replacement-${scenario}`, true, cwd);
545
583
  if (scenario === "off-branch") next.sessionManager.getBranch = () => [];
546
584
  await resumed.handlers.get("session_start")!({ reason: scenario }, next);
585
+ await settleStartup();
547
586
  const beforeEnd = statusCalls;
548
587
  await resumed.handlers.get("agent_end")!(agentEndEvent, next);
549
588
  assert.equal(statusCalls - beforeEnd, scenario === "reload" ? 1 : 0);
@@ -624,6 +663,7 @@ for (const mode of ["on", "off", "unknown", "missing", "malformed", "incompatibl
624
663
  const { handlers, sent } = harness(native);
625
664
  const session = ctx(`non-git-${mode}`, true, cwd);
626
665
  await handlers.get("session_start")!({}, session);
666
+ await settleStartup();
627
667
  assert.equal(requests.length, 0, "passive startup cannot bootstrap");
628
668
  assert.equal(existsSync(join(cwd, ".git")), false);
629
669
  await handlers.get("agent_end")!(agentEndEvent, session);
@@ -643,6 +683,7 @@ for (const path of ["odd/tasks/feature.md", "README.md", ".pi/settings.json", "m
643
683
  const h = harness({ reviewMode: onMode("on"), targetStatus: async () => { calls++; return stopStatus("unused"); } } as unknown as NativeReviewCli);
644
684
  const session = ctx("non-source", false, cwd);
645
685
  await h.handlers.get("session_start")!({}, session);
686
+ await settleStartup();
646
687
  await h.handlers.get("tool_result")!({ toolName: "write", toolCallId: "bookkeeping", input: { path }, isError: false }, session);
647
688
  assert.equal(calls, 0);
648
689
  assert.equal(existsSync(join(cwd, ".git")), false);
@@ -659,6 +700,7 @@ for (const initiallyGit of [true, false]) {
659
700
  } } as unknown as NativeReviewCli);
660
701
  const session = ctx("established-source", false, cwd);
661
702
  await h.handlers.get("session_start")!({}, session);
703
+ await settleStartup();
662
704
  if (!initiallyGit) { await directWrite(h.handlers, session); assert.equal(calls, 1); }
663
705
  await rename(join(cwd, ".git"), join(cwd, "saved-git"));
664
706
  calls = 0;
@@ -677,6 +719,7 @@ test("headless successful source write prepares after binding without a reminder
677
719
  } } as unknown as NativeReviewCli);
678
720
  const session = ctx("headless-source", false, cwd);
679
721
  await h.handlers.get("session_start")!({}, session);
722
+ await settleStartup();
680
723
  await directWrite(h.handlers, session);
681
724
  assert.equal(calls, 1);
682
725
  assert.equal(existsSync(join(cwd, ".git")), true);
@@ -690,7 +733,7 @@ test("session_start negotiates the current target identity when RDD is on", asyn
690
733
  const native = {
691
734
  reviewMode: onMode("on"),
692
735
  targetStatus: async (request: { agent?: string }) => {
693
- statusRequests.push(request);
736
+ statusRequests.push(withoutSignal(request));
694
737
  return executeStartStatus(targetIdentity);
695
738
  },
696
739
  } as unknown as NativeReviewCli;
@@ -701,9 +744,10 @@ test("session_start negotiates the current target identity when RDD is on", asyn
701
744
  const notifications: Array<{ message: string; severity: string }> = [];
702
745
  const session = {
703
746
  ...ctx("session-baseline-record", true, cwd),
704
- ui: { notify: (message: string, severity: string) => notifications.push({ message, severity }) },
747
+ ui: { ...ctx("session-baseline-record", true, cwd).ui, notify: (message: string, severity: string) => { notifications.push({ message, severity }); } },
705
748
  };
706
749
  await sessionStart!({}, session);
750
+ await settleStartup();
707
751
  assert.equal(statusRequests[0]?.agent, "pi");
708
752
  assert.deepEqual(sent, []);
709
753
  assert.deepEqual(notifications, []);
@@ -723,6 +767,8 @@ test("agent_end skips pre-session work, then nudges after an own mutation", asyn
723
767
  const session = ctx("session-baseline-skip", true, cwd);
724
768
 
725
769
  await sessionStart!({}, session);
770
+
771
+ await settleStartup();
726
772
  await agentEnd!(agentEndEvent, session);
727
773
  assert.deepEqual(sent, [], "the baseline candidate predates the session and is not nudged");
728
774
 
@@ -746,7 +792,7 @@ for (const ownsMutation of [false, true]) {
746
792
  const native = {
747
793
  reviewMode: onMode("on"),
748
794
  targetStatus: async (request: unknown) => {
749
- statusRequests.push(request);
795
+ statusRequests.push(withoutSignal(request));
750
796
  return executeStartStatus(targetIdentity);
751
797
  },
752
798
  } as unknown as NativeReviewCli;
@@ -760,6 +806,8 @@ for (const ownsMutation of [false, true]) {
760
806
  assert.equal(typeof agentEnd, "function");
761
807
 
762
808
  await sessionStart!({}, session);
809
+
810
+ await settleStartup();
763
811
  assert.deepEqual(statusRequests, [{ cwd, agent: "pi" }], "startup records targetA separately");
764
812
  assert.deepEqual(sent, []);
765
813
  // Like host event delivery, an event without a subscriber is a no-op.
@@ -810,7 +858,7 @@ test("agent_end reminds for an own write after startup with RDD off", async () =
810
858
  status: { global: "", cloneLocal: mode === "on" ? "on" : "", effective: mode, source: "clone_local" },
811
859
  }),
812
860
  targetStatus: async (request: unknown) => {
813
- statusRequests.push(request);
861
+ statusRequests.push(withoutSignal(request));
814
862
  if (mode === "off") throw new Error("targetStatus must not be called when RDD is off");
815
863
  return executeStartStatus(targetIdentity);
816
864
  },
@@ -822,6 +870,8 @@ test("agent_end reminds for an own write after startup with RDD off", async () =
822
870
  const session = ctx("session-baseline-rdd-off", true, cwd);
823
871
 
824
872
  await sessionStart!({}, session);
873
+
874
+ await settleStartup();
825
875
  assert.deepEqual(statusRequests, []);
826
876
 
827
877
  mode = "on";
@@ -870,6 +920,8 @@ test("session_shutdown does not turn the baseline into an own mutation", async (
870
920
  const session = ctx("session-baseline-shutdown", true, cwd);
871
921
 
872
922
  await sessionStart!({}, session);
923
+
924
+ await settleStartup();
873
925
  await agentEnd!(agentEndEvent, session);
874
926
  assert.deepEqual(sent, [], "the baseline candidate is skipped");
875
927
 
@@ -364,6 +364,55 @@ test("candidate owner publication never removes a replaced marker", (t) => {
364
364
  assert.equal(readdirSync(parent).some((name) => name.endsWith(".reaper-lock")), false);
365
365
  });
366
366
 
367
+ test("candidate owner publication preserves a replaced marker with 64-bit identity precision loss", (t) => {
368
+ mockWindowsAcl(t);
369
+ // The registry canonicalizes the contributor root (e.g. /var -> /private/var on macOS).
370
+ const cwd = repository(t), parent = join(realpathSync(cwd), ".git", "gentle-ai", "candidate-views");
371
+ const originalLstat = fs.lstatSync, fsync = fs.fsyncSync, write = fs.writeFileSync;
372
+ // Modeled identities, not measured NTFS values: distinct 64-bit indices round to one Number.
373
+ const high = 2n ** 53n;
374
+ assert.notEqual(high, high + 1n);
375
+ assert.equal(Number(high), Number(high + 1n));
376
+ let marker: string | undefined, replaced = false, faults = 0, adds = 0;
377
+ let savedContent: string | undefined;
378
+ t.mock.method(fs, "lstatSync", (path: Parameters<typeof fs.lstatSync>[0], ...args: unknown[]) => {
379
+ const stat = (originalLstat as (...arguments_: unknown[]) => ReturnType<typeof fs.lstatSync>)(path, ...args);
380
+ if (marker === undefined && typeof path === "string" && path.endsWith(".owner.json") && path === join(parent, readdirSync(parent).find((name) => name.endsWith(".owner.json")) ?? "")) marker = path;
381
+ if (path !== marker) return stat;
382
+ const bigint = (args[0] as { bigint?: boolean } | undefined)?.bigint === true;
383
+ const ino = replaced ? high + 1n : high;
384
+ // Retain real metadata and Stats methods; only the identity pair is synthetic.
385
+ return Object.assign(Object.create(Object.getPrototypeOf(stat)), stat, {
386
+ dev: bigint ? high : Number(high), ino: bigint ? ino : Number(ino),
387
+ });
388
+ });
389
+ t.mock.method(fs, "fsyncSync", (fd: number) => {
390
+ // The reaper lock is synced before marker publication; it is not this fault's boundary.
391
+ if (marker === undefined || replaced) return fsync(fd);
392
+ assert.ok(marker, "owned marker identity must be captured before the fault");
393
+ faults++;
394
+ savedContent = readFileSync(marker, "utf8");
395
+ renameSync(marker, `${marker}.saved`);
396
+ write(marker, "replacement", { mode: 0o600 });
397
+ replaced = true;
398
+ throw new Error("fixture marker fsync failure after 64-bit identity replacement");
399
+ });
400
+ syncBuiltinESMExports();
401
+ t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
402
+ const registry = new CandidateViewRegistry((file, args, options) => {
403
+ if (args[0] === "worktree" && args[1] === "add") adds++;
404
+ return execFileSync(file, args, options);
405
+ }, "win32");
406
+ assert.throws(() => registry.create({ contributorRoot: cwd }), (error: unknown) => error instanceof CandidateViewError && error.reason === "candidate-owner-preparation-failed");
407
+ assert.equal(faults, 1, "replacement fault must be reached exactly once");
408
+ assert.equal(adds, 0);
409
+ assert.ok(marker);
410
+ assert.equal(readFileSync(`${marker}.saved`, "utf8"), savedContent);
411
+ assert.equal(readdirSync(parent).some((name) => name.endsWith(".reaper-lock")), false);
412
+ assert.equal(existsSync(marker), true, "publication rollback deleted the replacement after distinct 64-bit identities rounded to the same Number");
413
+ assert.equal(readFileSync(marker, "utf8"), "replacement");
414
+ });
415
+
367
416
  test("public POSIX candidate-views parent reports bounded privacy guidance without changing permissions or adding a worktree", { skip: process.platform === "win32" }, (t) => {
368
417
  const cwd = repository(t);
369
418
  const parent = join(cwd, ".git", "gentle-ai", "candidate-views");
@@ -721,6 +770,47 @@ for (const race of ["root", "registration", "lock"] as const) {
721
770
  });
722
771
  }
723
772
 
773
+ test("ordinary cleanup preserves a replaced root with 64-bit identity precision loss", (t) => {
774
+ mockWindowsAcl(t);
775
+ const cwd = repository(t), high = 2n ** 53n;
776
+ const originalLstat = fs.lstatSync;
777
+ let root = "", replaced = false, lists = 0, removes = 0;
778
+ const registry = new CandidateViewRegistry((file, args, options) => {
779
+ // Never allow Git to delete the replacement, even on the failing implementation.
780
+ if (args[0] === "worktree" && args[1] === "remove") { removes++; return ""; }
781
+ const result = execFileSync(file, args, options);
782
+ if (root && args[0] === "worktree" && args[1] === "list" && ++lists === 2) {
783
+ const pointer = readFileSync(join(root, ".git"));
784
+ renameSync(root, `${root}.saved`);
785
+ mkdirSync(root);
786
+ writeFileSync(join(root, ".git"), pointer);
787
+ writeFileSync(join(root, "replacement.txt"), "replacement");
788
+ replaced = true;
789
+ }
790
+ return result;
791
+ });
792
+ const view = registry.create({ contributorRoot: cwd });
793
+ root = view.root;
794
+ t.mock.method(fs, "lstatSync", (path: Parameters<typeof fs.lstatSync>[0], ...args: unknown[]) => {
795
+ const stat = (originalLstat as (...arguments_: unknown[]) => ReturnType<typeof fs.lstatSync>)(path, ...args);
796
+ if (path !== root) return stat;
797
+ const bigint = (args[0] as { bigint?: boolean } | undefined)?.bigint === true;
798
+ const ino = replaced ? high + 1n : high;
799
+ return Object.assign(Object.create(Object.getPrototypeOf(stat)), stat, {
800
+ dev: bigint ? high : Number(high), ino: bigint ? ino : Number(ino),
801
+ });
802
+ });
803
+ syncBuiltinESMExports();
804
+ t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
805
+ assert.throws(() => view.cleanup());
806
+ assert.equal(replaced, true);
807
+ assert.equal(readFileSync(join(root, "replacement.txt"), "utf8"), "replacement");
808
+ assert.equal(existsSync(`${root}.saved`), true);
809
+ assert.equal(existsSync(ownerMarker(root)), true);
810
+ assert.equal(existsSync(`${root}.reaper-lock`), false);
811
+ assert.equal(removes, 0, "distinct 64-bit directory identities must stop cleanup before Git removal");
812
+ });
813
+
724
814
  test("ordinary cleanup never unlinks a replaced sidecar after Git removal", (t) => {
725
815
  const cwd = repository(t);
726
816
  let marker = "";
@@ -1193,22 +1283,21 @@ test("candidate view skips a shared index that disappears during stat or copy",
1193
1283
  for (const phase of ["stat", "copy"] as const) {
1194
1284
  const originalLstatSync = fs.lstatSync;
1195
1285
  const originalCopyFileSync = fs.copyFileSync;
1196
- fs.lstatSync = ((path: string | Buffer, options?: Parameters<typeof lstatSync>[1]) => {
1286
+ t.mock.method(fs, "lstatSync", (path: string | Buffer, options?: Parameters<typeof lstatSync>[1]) => {
1197
1287
  if (phase === "stat" && path === sharedIndexPath) throw Object.assign(new Error("shared index disappeared"), { code: "ENOENT" });
1198
1288
  return originalLstatSync(path, options);
1199
- }) as typeof fs.lstatSync;
1200
- fs.copyFileSync = ((source: string | Buffer, destination: string | Buffer) => {
1289
+ });
1290
+ t.mock.method(fs, "copyFileSync", (source: string | Buffer, destination: string | Buffer) => {
1201
1291
  if (phase === "copy" && source === sharedIndexPath) throw Object.assign(new Error("shared index disappeared"), { code: "ENOENT" });
1202
1292
  return originalCopyFileSync(source, destination);
1203
- }) as typeof fs.copyFileSync;
1293
+ });
1204
1294
  syncBuiltinESMExports();
1205
1295
  let view: ReturnType<typeof createCandidateView> | undefined;
1206
1296
  try {
1207
1297
  view = createCandidateView({ contributorRoot, intendedUntracked: [] });
1208
1298
  assert.equal(view.paths.includes("staged-addition.txt"), true, phase);
1209
1299
  } finally {
1210
- fs.lstatSync = originalLstatSync;
1211
- fs.copyFileSync = originalCopyFileSync;
1300
+ t.mock.restoreAll();
1212
1301
  syncBuiltinESMExports();
1213
1302
  view?.cleanup();
1214
1303
  }
@@ -1435,13 +1524,13 @@ test("corrected views stay within frozen scope and replace projections only when
1435
1524
  const registry = new CandidateViewRegistry();
1436
1525
  const initial = registry.create({ contributorRoot }); registry.bind({ token: initial.token, lineageId: "correction", selectedLenses: ["review-risk"] });
1437
1526
  writeFileSync(join(contributorRoot, "tracked.txt"), "corrected\n");
1438
- const corrected = registry.createCorrected("correction", contributorRoot);
1527
+ const corrected = registry.createCorrected("correction", contributorRoot, "corrected-1");
1439
1528
  assert.notEqual(corrected.candidateTree, initial.candidateTree);
1440
1529
  assert.equal(registry.resolveProjection("correction", contributorRoot).candidateTree, initial.candidateTree);
1441
1530
  registry.promoteCorrected("correction", corrected.token);
1442
1531
  assert.equal(registry.resolveProjection("correction", contributorRoot).candidateTree, corrected.candidateTree);
1443
1532
  writeFileSync(join(contributorRoot, "escaped.txt"), "outside scope\n");
1444
- assert.throws(() => registry.createCorrected("correction", contributorRoot), /escapes the frozen genesis paths/);
1533
+ assert.throws(() => registry.createCorrected("correction", contributorRoot, "escaped-2"), /escapes the frozen genesis paths/);
1445
1534
  registry.cleanupTerminal("correction", "approved");
1446
1535
  });
1447
1536
 
@@ -1664,7 +1753,7 @@ test("candidate view compacts an oversized non-ASCII scope losslessly and determ
1664
1753
  assert.throws(() => readCandidateContextManifestPage(compact.encoded, compact.sha256, actorEntries.length + 1), /cursor/);
1665
1754
  const nonCanonicalBytes = Buffer.from(JSON.stringify({ gitlinks: decoded.manifest.gitlinks, scopeByMode: decoded.manifest.scopeByMode, version: 1 }), "utf8");
1666
1755
  assert.throws(
1667
- () => decodeCandidateContextManifest(gzipSync(nonCanonicalBytes, { mtime: 0 }).toString("base64url"), createHash("sha256").update(nonCanonicalBytes).digest("hex")),
1756
+ () => decodeCandidateContextManifest(gzipSync(nonCanonicalBytes).toString("base64url"), createHash("sha256").update(nonCanonicalBytes).digest("hex")),
1668
1757
  /canonical/,
1669
1758
  );
1670
1759
  } finally {
@@ -1682,7 +1771,7 @@ test("candidate context manifest decoder accepts canonical numeric-looking gitli
1682
1771
  scopeByMode: { "160000": ["10", "2"] },
1683
1772
  gitlinks,
1684
1773
  }), "utf8");
1685
- const encoded = gzipSync(bytes, { mtime: 0 }).toString("base64url");
1774
+ const encoded = gzipSync(bytes).toString("base64url");
1686
1775
  assert.deepEqual(decodeCandidateContextManifest(encoded, createHash("sha256").update(bytes).digest("hex")).manifest, {
1687
1776
  version: 1,
1688
1777
  scopeByMode: { "160000": ["10", "2"] },
@@ -1701,7 +1790,7 @@ test("candidate context manifest decoder rejects noncanonical nonnumeric gitlink
1701
1790
  gitlinks,
1702
1791
  }), "utf8");
1703
1792
  assert.throws(
1704
- () => decodeCandidateContextManifest(gzipSync(bytes, { mtime: 0 }).toString("base64url"), createHash("sha256").update(bytes).digest("hex")),
1793
+ () => decodeCandidateContextManifest(gzipSync(bytes).toString("base64url"), createHash("sha256").update(bytes).digest("hex")),
1705
1794
  /canonical/,
1706
1795
  );
1707
1796
  });
@@ -1709,7 +1798,7 @@ test("candidate context manifest decoder rejects noncanonical nonnumeric gitlink
1709
1798
  test("candidate context manifest decoder rejects noncanonical gzip transport for verified bytes", () => {
1710
1799
  const bytes = Buffer.from(JSON.stringify({ version: 1, scopeByMode: { "100644": ["file.ts"] }, gitlinks: {} }), "utf8");
1711
1800
  const sha256 = createHash("sha256").update(bytes).digest("hex");
1712
- const canonical = gzipSync(bytes, { mtime: 0 });
1801
+ const canonical = gzipSync(bytes);
1713
1802
  const noncanonical = Buffer.from(canonical);
1714
1803
  noncanonical[4] = (noncanonical[4]! + 1) & 0xff;
1715
1804
  assert.throws(() => decodeCandidateContextManifest(noncanonical.toString("base64url"), sha256), /canonical/);
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import test, { after, before } from "node:test";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
- import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
9
+ import { createGentleAiExtension, __testing } from "../extensions/gentle-ai.ts";
10
10
  import type { NativeReviewCli } from "../lib/native-review-cli.ts";
11
11
 
12
12
  // gentle-pi#560 / gentle-ai#4056, #4057: since 2026-08-01 Gentle AI stopped
@@ -103,7 +103,7 @@ test("before_agent_start injects the mirrored review execution contract for the
103
103
  assert.equal(result, undefined, "the handler must not return a replacement systemPrompt");
104
104
  const appended = event.systemPromptOptions.appendSystemPrompt;
105
105
  const expected = mirroredPiOrchestrationText();
106
- assert.match(appended, /Substantial authorized work: use ODD/);
106
+ assert.match(appended, /large tasks get ODD tracking and workers/);
107
107
  assert.match(appended, /For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks/);
108
108
  assert.match(appended, /Test presence alone does not establish applicability; no chat or TUI toggle activates it/);
109
109
  assert.match(appended, /no meaningful RED, explain why and run proportionate ordinary functional or structural verification/);
@@ -111,7 +111,7 @@ test("before_agent_start injects the mirrored review execution contract for the
111
111
  assert.doesNotMatch(appended, /If tests exist, use strict TDD/);
112
112
  assert.match(appended, /ODD \(Default Workflow, harness section above\) is mandatory on every request/);
113
113
  assert.doesNotMatch(appended, /Prefer SDD\/OpenSpec artifacts/);
114
- assert.match(appended, /## Gentle AI review execution contract \(mirrored provider bundle 1\.2\.0\)/);
114
+ assert.match(appended, /## Gentle AI review execution contract \(mirrored provider bundle 1\.3\.0\)/);
115
115
  assert.ok(appended.includes(expected), "the mirrored orchestration/pi.md text must appear verbatim");
116
116
  assert.match(appended, /call `gentle_review` with {"operation":"inspect"}/);
117
117
  assert.match(appended, /call `gentle_review` with operation `status`, the exact retained `lineageId`, and `workspaceRoot`/);
@@ -162,7 +162,7 @@ test("before_agent_start does not let legacy prompt text bypass primary ODD and
162
162
  const event = primaryEvent({ systemPrompt: "SDD apply executor body" });
163
163
  await beforeAgentStart(event, ctx());
164
164
  const appended = event.systemPromptOptions.appendSystemPrompt;
165
- assert.match(appended, /Substantial authorized work: use ODD/);
165
+ assert.match(appended, /large tasks get ODD tracking and workers/);
166
166
  assert.match(appended, /Gentle AI review execution contract/);
167
167
  assert.doesNotMatch(appended, /### 3\. SDD \(optional\)/);
168
168
  });
@@ -228,3 +228,32 @@ test("rejects a tampered mirror, accepts a matching one, and warns once", async
228
228
  rmSync(matching, { recursive: true, force: true });
229
229
  }
230
230
  });
231
+
232
+ // gentle-shell#1494 F2: when receipt-driven development is off, the review
233
+ // execution contract is irrelevant to the session, so it is not loaded. On and
234
+ // unknown keep it (unknown fails safe toward the reviewed path).
235
+ function rddCli(effective: "on" | "off" | "throws"): NativeReviewCli {
236
+ return {
237
+ reviewMode: async () => {
238
+ if (effective === "throws") throw new Error("native review mode is unavailable");
239
+ return { operation: "status", scope: "clone", status: { global: effective, cloneLocal: "", effective, source: "global" } };
240
+ },
241
+ } as unknown as NativeReviewCli;
242
+ }
243
+
244
+ for (const [effective, injected] of [["off", false], ["on", true], ["throws", true]] as const) {
245
+ test(`before_agent_start ${injected ? "injects" : "skips"} the review execution contract when RDD is ${effective === "throws" ? "unknown" : effective}`, async () => {
246
+ __testing.clearRddStatusMemoForTesting();
247
+ try {
248
+ const { beforeAgentStart } = harness(rddCli(effective));
249
+ const event = primaryEvent();
250
+ await beforeAgentStart(event, ctx());
251
+ const appended = event.systemPromptOptions.appendSystemPrompt;
252
+ assert.match(appended, /# el Gentleman Orchestrator/, "the harness itself is always injected");
253
+ if (injected) assert.match(appended, /Gentle AI review execution contract/);
254
+ else assert.doesNotMatch(appended, /Gentle AI review execution contract/);
255
+ } finally {
256
+ __testing.clearRddStatusMemoForTesting();
257
+ }
258
+ });
259
+ }