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
@@ -0,0 +1,269 @@
1
+ import assert from "node:assert/strict";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import test, { type TestContext } from "node:test";
7
+ import { fileURLToPath } from "node:url";
8
+ import { DISTRIBUTION_ASSETS, claimPrefix, distributionAsset, distributionFiles, installVersion, pnpmEnvironment, readDistribution } from "../scripts/bundled-install.mjs";
9
+ import { buildDistribution, main, verifyDistribution } from "../scripts/build-distribution.mjs";
10
+ import { artifactFor } from "../scripts/installer-downloads.mjs";
11
+ import { PI_INSTALL_VERSION } from "../scripts/installer-preflight.mjs";
12
+
13
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
14
+ // Real directories, modes and uids need a POSIX host.
15
+ const posixHost = process.platform === "win32" ? "POSIX filesystem fixtures need a POSIX host" : false;
16
+ const posixTest = (name: string, fn: (t: TestContext) => void | Promise<void>) => test(name, { skip: posixHost }, fn);
17
+ const manifest = { shell: "4.1.0", pi: "1.0.2" };
18
+ const LOCKFILE = "lockfileVersion: '9.0'\n\nimporters:\n\n .: {}\n";
19
+ const sha256 = (text: string) => createHash("sha256").update(text, "utf8").digest("hex");
20
+
21
+ /** A new folder in the system tmpdir, removed when the test ends. */
22
+ function temporary(t: TestContext) {
23
+ const root = mkdtempSync(join(realpathSync(tmpdir()), "build-distribution-"));
24
+ t.after(() => rmSync(root, { recursive: true, force: true }));
25
+ return root;
26
+ }
27
+ function claimed(t: TestContext) {
28
+ const home = join(temporary(t), "home");
29
+ mkdirSync(home, { mode: 0o700 });
30
+ return claimPrefix({ platform: "darwin", env: { HOME: home }, home });
31
+ }
32
+ type Run = { command: string; args: string[]; cwd: string; env: Record<string, string>; files: Record<string, string> };
33
+ /** Our pnpm resolving the lockfile: it records what it saw, writes the lockfile,
34
+ * and, like pnpm 11 after skipped builds, may rewrite pnpm-workspace.yaml. */
35
+ function fakeResolver({ lockfile = LOCKFILE, rewrite = false, status = 0, missing = 0 } = {}) {
36
+ const runs: Run[] = [];
37
+ const run = (command: string, args: string[], { cwd, env }: { cwd: string; env: Record<string, string> }) => {
38
+ runs.push({ command, args, cwd, env, files: Object.fromEntries(readdirSync(cwd).map((name) => [name, readFileSync(join(cwd, name), "utf8")])) });
39
+ // As pnpm 11.1.1 prints it, on stdout, before the registry lists a just-published version.
40
+ const name = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")).version;
41
+ if (runs.length <= missing) return { status: 1, stdout: `[ERR_PNPM_NO_MATCHING_VERSION] No matching version found for gentle-pi@${name} while fetching it from https://registry.npmjs.org/\n`, stderr: "" };
42
+ if (status !== 0) return { status, stdout: "", stderr: "ERR_PNPM_FETCH_404" };
43
+ writeFileSync(join(cwd, "pnpm-lock.yaml"), lockfile);
44
+ if (rewrite) writeFileSync(join(cwd, "pnpm-workspace.yaml"), "allowBuilds:\n gentle-pi: true\n koffi: set this to true or false\nstrictDepBuilds: false\n");
45
+ return { status: 0, stdout: "", stderr: "" };
46
+ };
47
+ return { runs, run };
48
+ }
49
+
50
+ test("the distribution asset records the exact versions, the pins, the workspace files installVersion writes and the lockfile sha256", () => {
51
+ const text = distributionAsset(manifest, LOCKFILE);
52
+ assert.deepEqual(DISTRIBUTION_ASSETS, { distribution: "gentle-shell-distribution.json", lockfile: "gentle-shell-distribution-lock.yaml" });
53
+ assert.deepEqual(JSON.parse(text), {
54
+ schema: 1, shell: "4.1.0", pi: "1.0.2", id: "4.1.0-1.0.2",
55
+ generatedWith: { node: artifactFor("node", "linux", "x64").version, pnpm: artifactFor("pnpm").version },
56
+ files: distributionFiles(manifest),
57
+ lockfile: { name: "gentle-shell-distribution-lock.yaml", sha256: sha256(LOCKFILE) },
58
+ });
59
+ assert.equal(text.endsWith("}\n"), true);
60
+ assert.equal(distributionAsset({ ...manifest }, `${LOCKFILE}`), text, "the same inputs give the same bytes");
61
+ assert.notEqual(distributionAsset(manifest, `${LOCKFILE}\n`), text);
62
+ for (const bad of [{ shell: "^4.1.0", pi: "1.0.2" }, { shell: "4.1", pi: "1.0.2" }, { shell: "4.1.0", pi: "latest" }, { shell: "4.1.0", pi: "v1.0.2" }, { shell: "4.1.0", pi: "1.x" }]) {
63
+ assert.throws(() => distributionAsset(bad, LOCKFILE), /version rejected/, JSON.stringify(bad));
64
+ }
65
+ assert.throws(() => distributionAsset(manifest, ""), /lockfile/i);
66
+ });
67
+
68
+ test("readDistribution accepts only an unchanged pair whose workspace files are the ones installVersion writes", () => {
69
+ const text = distributionAsset(manifest, LOCKFILE);
70
+ assert.deepEqual(readDistribution(text, LOCKFILE), { manifest, lockfile: LOCKFILE });
71
+ assert.throws(() => readDistribution(text, `${LOCKFILE}# changed\n`), /lockfile sha256/);
72
+ const asset = JSON.parse(text);
73
+ const tampered = (change: (value: typeof asset) => void) => { const copy = structuredClone(asset); change(copy); return `${JSON.stringify(copy, null, "\t")}\n`; };
74
+ assert.throws(() => readDistribution(tampered((value) => { value.files["pnpm-workspace.yaml"] = "allowBuilds:\n koffi: true\n"; }), LOCKFILE), /workspace files/);
75
+ assert.throws(() => readDistribution(tampered((value) => { value.schema = 2; }), LOCKFILE), /schema/);
76
+ assert.throws(() => readDistribution(tampered((value) => { value.pi = "^1.0.2"; }), LOCKFILE), /version rejected/);
77
+ assert.throws(() => readDistribution("not json", LOCKFILE), /Distribution asset rejected/);
78
+ // A release built with other pinned runtimes still installs with ours: only recorded, not compared.
79
+ assert.deepEqual(readDistribution(tampered((value) => { value.generatedWith.pnpm = "11.2.0"; }), LOCKFILE).manifest, manifest);
80
+ });
81
+
82
+ posixTest("buildDistribution resolves the lockfile with our pnpm from the unmodified distribution files and returns both assets", async (t) => {
83
+ const layout = claimed(t);
84
+ const resolver = fakeResolver({ rewrite: true });
85
+ const env = { PATH: "/usr/bin", npm_config_registry: "http://user.example" };
86
+ const result = await buildDistribution({ layout, shell: "4.1.0", pi: "1.0.2", env, adapters: { run: resolver.run } });
87
+ assert.equal(resolver.runs.length, 1);
88
+ const [run] = resolver.runs;
89
+ assert.equal(run.command, layout.node);
90
+ assert.deepEqual(run.args, [layout.pnpm, "install", "--lockfile-only"]);
91
+ assert.deepEqual(run.files, distributionFiles(manifest), "pnpm resolves exactly the files installVersion writes, and no lockfile");
92
+ assert.deepEqual(run.env, pnpmEnvironment(layout, { env }));
93
+ assert.equal(dirname(run.cwd), layout.tmp);
94
+ assert.equal(existsSync(run.cwd), false, "the resolution folder is removed");
95
+ assert.deepEqual(result.manifest, manifest);
96
+ assert.deepEqual(Object.keys(result.assets).sort(), [DISTRIBUTION_ASSETS.distribution, DISTRIBUTION_ASSETS.lockfile].sort());
97
+ assert.equal(result.assets[DISTRIBUTION_ASSETS.lockfile], LOCKFILE);
98
+ // pnpm's rewritten workspace file (placeholders for skipped builds) never reaches the asset.
99
+ assert.equal(result.assets[DISTRIBUTION_ASSETS.distribution], distributionAsset(manifest, LOCKFILE));
100
+ assert.equal(JSON.parse(result.assets[DISTRIBUTION_ASSETS.distribution]).lockfile.sha256, sha256(LOCKFILE));
101
+ const again = await buildDistribution({ layout, shell: "4.1.0", pi: "1.0.2", env, adapters: { run: fakeResolver({ rewrite: true }).run } });
102
+ assert.deepEqual(again.assets, result.assets, "the same lockfile gives byte-identical assets");
103
+ });
104
+
105
+ posixTest("buildDistribution pins Pi to PI_INSTALL_VERSION and refuses a non-exact version or a failed resolution before publishing anything", async (t) => {
106
+ const layout = claimed(t);
107
+ const pinned = await buildDistribution({ layout, shell: "4.1.0", adapters: { run: fakeResolver().run } });
108
+ assert.deepEqual(pinned.manifest, { shell: "4.1.0", pi: PI_INSTALL_VERSION });
109
+ assert.equal(JSON.parse(pinned.assets[DISTRIBUTION_ASSETS.distribution]).pi, PI_INSTALL_VERSION);
110
+ for (const [shell, pi] of [["^4.1.0", "1.0.2"], ["4.1.0", "~1.0.2"], ["latest", "1.0.2"], ["4.1.0", "1.0.2 || 1.0.3"], ["4.1.0+build", "1.0.2"]]) {
111
+ const resolver = fakeResolver();
112
+ await assert.rejects(buildDistribution({ layout, shell, pi, adapters: { run: resolver.run } }), /version rejected/, `${shell} ${pi}`);
113
+ assert.equal(resolver.runs.length, 0, "pnpm never runs for a non-exact version");
114
+ }
115
+ const failed = fakeResolver({ status: 1 });
116
+ await assert.rejects(buildDistribution({ layout, shell: "4.1.0", pi: "1.0.2", adapters: { run: failed.run, sleep: async () => {} } }),
117
+ (error: Error & { output?: string }) => /lockfile resolution failed/.test(error.message) && error.output === "ERR_PNPM_FETCH_404");
118
+ assert.equal(failed.runs.length, 1, "only a gentle-pi missing from the registry is retried");
119
+ await assert.rejects(buildDistribution({ layout, shell: "4.1.0", pi: "1.0.2", adapters: { run: fakeResolver({ lockfile: "" }).run } }), /lockfile/i);
120
+ assert.deepEqual(readdirSync(layout.tmp), [], "no resolution folder is left behind");
121
+ });
122
+
123
+ posixTest("the assets install with installVersion: it writes the recorded workspace files and the lockfile byte for byte", async (t) => {
124
+ const layout = claimed(t);
125
+ const { assets } = await buildDistribution({ layout, shell: "4.1.0", pi: "1.0.2", adapters: { run: fakeResolver().run } });
126
+ const { manifest: read, lockfile } = readDistribution(assets[DISTRIBUTION_ASSETS.distribution], assets[DISTRIBUTION_ASSETS.lockfile]);
127
+ let written: Record<string, string> = {};
128
+ const run = (_command: string, args: string[], { cwd }: { cwd: string }) => {
129
+ assert.deepEqual(args.slice(1), ["install", "--frozen-lockfile"]);
130
+ written = Object.fromEntries(["package.json", "pnpm-workspace.yaml", "pnpm-lock.yaml"].map((name) => [name, readFileSync(join(cwd, name), "utf8")]));
131
+ const shell = join(cwd, "node_modules", "gentle-pi"); const pi = join(cwd, "node_modules", "@earendil-works", "pi-coding-agent");
132
+ mkdirSync(join(shell, "bin"), { recursive: true }); mkdirSync(pi, { recursive: true });
133
+ writeFileSync(join(shell, "package.json"), JSON.stringify({ name: "gentle-pi", version: read.shell }));
134
+ writeFileSync(join(shell, "bin", "gentle-shell.mjs"), "");
135
+ writeFileSync(join(pi, "package.json"), JSON.stringify({ name: "@earendil-works/pi-coding-agent", version: read.pi }));
136
+ return { status: 0, stdout: "", stderr: "" };
137
+ };
138
+ await installVersion({ layout, manifest: read, lockfile, platform: "darwin", env: {}, adapters: { run } });
139
+ const recorded = JSON.parse(assets[DISTRIBUTION_ASSETS.distribution]).files;
140
+ assert.deepEqual(written, { ...recorded, "pnpm-lock.yaml": assets[DISTRIBUTION_ASSETS.lockfile] });
141
+ });
142
+
143
+ posixTest("verifyDistribution installs the pair with our runtimes, activates it and requires the launcher to report both exact versions", async (t) => {
144
+ const layout = claimed(t);
145
+ // Runtimes already published under their pinned URLs are reused, so nothing is downloaded.
146
+ for (const [name, directory, entry] of [["node", layout.nodeDir, layout.node], ["pnpm", layout.pnpmDir, layout.pnpm]]) {
147
+ mkdirSync(dirname(entry), { recursive: true });
148
+ writeFileSync(entry, "");
149
+ writeFileSync(join(directory, ".gentle-shell-runtime"), `${artifactFor(name, "darwin", "arm64").url}\n`);
150
+ }
151
+ const distribution = distributionAsset(manifest, LOCKFILE);
152
+ const installs: string[][] = [];
153
+ const run = (_command: string, args: string[], { cwd }: { cwd: string }) => {
154
+ installs.push(args.slice(1));
155
+ const shell = join(cwd, "node_modules", "gentle-pi"); const pi = join(cwd, "node_modules", "@earendil-works", "pi-coding-agent");
156
+ mkdirSync(join(shell, "bin"), { recursive: true }); mkdirSync(pi, { recursive: true });
157
+ writeFileSync(join(shell, "package.json"), JSON.stringify({ name: "gentle-pi", version: manifest.shell }));
158
+ writeFileSync(join(shell, "bin", "gentle-shell.mjs"), "");
159
+ writeFileSync(join(pi, "package.json"), JSON.stringify({ name: "@earendil-works/pi-coding-agent", version: manifest.pi }));
160
+ return { status: 0, stdout: "", stderr: "" };
161
+ };
162
+ const launches: string[] = [];
163
+ const launch = (stdout: string) => (launcher: string) => { launches.push(launcher); return { status: 0, stdout, stderr: "" }; };
164
+ const options = { layout, distribution, lockfile: LOCKFILE, platform: "darwin", arch: "arm64", env: {} };
165
+ const result = await verifyDistribution({ ...options, adapters: { run, launch: launch("gentle-shell 4.1.0\npi 1.0.2\nhome isolated /h\n") } });
166
+ assert.deepEqual(installs, [["install", "--frozen-lockfile"]]);
167
+ assert.deepEqual(launches, [layout.launcher]);
168
+ assert.deepEqual(result, { id: "4.1.0-1.0.2", version: "gentle-shell 4.1.0\npi 1.0.2\nhome isolated /h" });
169
+ assert.equal(readFileSync(layout.launcher, "utf8").includes("gentle-shell bundled launcher"), true);
170
+ // Another Pi (a fallback) or another gentle-shell fails the verification.
171
+ await assert.rejects(verifyDistribution({ ...options, adapters: { run, launch: launch("gentle-shell 4.1.0\npi 1.1.0\n") } }), /launcher reported/);
172
+ await assert.rejects(verifyDistribution({ ...options, adapters: { run, launch: () => ({ status: 1, stdout: "", stderr: "boom" }) } }), /launcher reported/);
173
+ await assert.rejects(verifyDistribution({ ...options, lockfile: `${LOCKFILE}x`, adapters: { run, launch: launch("") } }), /lockfile sha256/);
174
+ // The launcher runs in the same filtered environment as our pnpm, without Pi or Gentle overrides.
175
+ const host = { HOME: "/h", PATH: "/usr/bin", NODE_OPTIONS: "--require /x.js", NODE_PATH: "/x", npm_config_prefix: "/x", PNPM_HOME: "/x", PI_CODING_AGENT_DIR: "/x", GENTLE_SHELL_PI: "/x" };
176
+ let seen: Record<string, string> = {};
177
+ await verifyDistribution({ ...options, env: host, adapters: { run, launch: (_launcher: string, { env }: { env: Record<string, string> }) => {
178
+ seen = env; return { status: 0, stdout: "gentle-shell 4.1.0\npi 1.0.2\n", stderr: "" };
179
+ } } });
180
+ const { PI_CODING_AGENT_DIR: _pi, GENTLE_SHELL_PI: _shell, ...kept } = host;
181
+ assert.deepEqual(seen, pnpmEnvironment(layout, { platform: "darwin", env: kept }));
182
+ for (const key of ["NODE_OPTIONS", "NODE_PATH", "npm_config_prefix", "PI_CODING_AGENT_DIR", "GENTLE_SHELL_PI"]) assert.equal(key in seen, false, key);
183
+ assert.equal(seen.PATH.split(":")[0], dirname(layout.node), "our Node first on PATH");
184
+ });
185
+
186
+ posixTest("buildDistribution retries while the just-published gentle-pi is not on the registry, then fails clearly", async (t) => {
187
+ const layout = claimed(t);
188
+ const waits: number[] = [];
189
+ const sleep = async (ms: number) => { waits.push(ms); };
190
+ const late = fakeResolver({ missing: 2 });
191
+ const result = await buildDistribution({ layout, shell: "4.1.0", pi: "1.0.2", adapters: { run: late.run, sleep } });
192
+ assert.equal(result.assets[DISTRIBUTION_ASSETS.lockfile], LOCKFILE);
193
+ assert.equal(late.runs.length, 3);
194
+ assert.deepEqual(waits, [30000, 30000]);
195
+ assert.equal(new Set(late.runs.map((run) => run.cwd)).size, 3, "each attempt resolves in a fresh folder");
196
+ for (const run of late.runs) assert.deepEqual(run.files, distributionFiles(manifest));
197
+ waits.length = 0;
198
+ const never = fakeResolver({ missing: 99 });
199
+ await assert.rejects(buildDistribution({ layout, shell: "4.1.0", pi: "1.0.2", adapters: { run: never.run, sleep } }),
200
+ (error: Error & { output?: string }) => /gentle-pi@4\.1\.0 is not on the registry after 10 attempts/.test(error.message) && /ERR_PNPM_NO_MATCHING_VERSION/.test(error.output ?? ""));
201
+ assert.equal(never.runs.length, 10);
202
+ assert.equal(waits.reduce((sum, ms) => sum + ms, 0), 270000, "about five minutes in all");
203
+ // A missing Pi, or another gentle-pi version, is a real failure: never retried.
204
+ for (const missing of ["@earendil-works/pi-coding-agent@1.0.2", "gentle-pi@4.1.0-beta.1", "gentle-pi@4.1.01"]) {
205
+ let runs = 0;
206
+ const run = () => { runs += 1; return { status: 1, stdout: `[ERR_PNPM_NO_MATCHING_VERSION] No matching version found for ${missing} while fetching it\n`, stderr: "" }; };
207
+ await assert.rejects(buildDistribution({ layout, shell: "4.1.0", pi: "1.0.2", adapters: { run, sleep } }), /lockfile resolution failed/, missing);
208
+ assert.equal(runs, 1, missing);
209
+ }
210
+ assert.deepEqual(readdirSync(layout.tmp), [], "no resolution folder is left behind");
211
+ });
212
+
213
+ posixTest("the CLI removes the temporary prefix it created when build or verify fails, and refuses bad input before creating one", async (t) => {
214
+ const root = temporary(t);
215
+ const options = { temporaryRoot: root, env: { PATH: "/usr/bin" }, adapters: { download: async () => Buffer.from("not the pinned archive") } };
216
+ await assert.rejects(main(["verify", "--assets", join(root, "missing")], options), /ENOENT/);
217
+ assert.deepEqual(readdirSync(root), [], "verify with unreadable assets");
218
+ const assets = join(root, "assets"); mkdirSync(assets);
219
+ writeFileSync(join(assets, DISTRIBUTION_ASSETS.distribution), distributionAsset(manifest, LOCKFILE));
220
+ writeFileSync(join(assets, DISTRIBUTION_ASSETS.lockfile), LOCKFILE);
221
+ await assert.rejects(main(["verify", "--assets", assets], options), /Download integrity mismatch/);
222
+ assert.deepEqual(readdirSync(root), ["assets"], "verify whose runtime download fails");
223
+ await assert.rejects(main(["build", "--shell", "4.1.0", "--out", join(root, "out")], options), /Download integrity mismatch/);
224
+ assert.deepEqual(readdirSync(root), ["assets"], "build whose runtime download fails");
225
+ await assert.rejects(main(["build", "--shell", "^4.1.0", "--out", join(root, "out")], options), /version rejected/);
226
+ await assert.rejects(main(["publish"], options), /Usage/);
227
+ assert.deepEqual(readdirSync(root), ["assets"]);
228
+ });
229
+
230
+ // publish.yml: the distribution assets reach the release only after the frozen
231
+ // install passed on all three systems, and the existing jobs keep their shape.
232
+ function jobs(workflow: string) {
233
+ const body = workflow.slice(workflow.indexOf("\njobs:\n") + "\njobs:\n".length);
234
+ return new Map([...body.matchAll(/^ {2}([A-Za-z0-9_-]+):\n([\s\S]*?)(?=^ {2}[A-Za-z0-9_-]+:\n|(?![\s\S]))/gm)].map((match) => [match[1], match[2]]));
235
+ }
236
+ test("publish.yml builds the distribution after npm publication, verifies it on three systems and only then uploads it", () => {
237
+ const workflow = readFileSync(join(ROOT, ".github", "workflows", "publish.yml"), "utf8");
238
+ const all = jobs(workflow);
239
+ assert.deepEqual([...all.keys()], ["publish", "installers", "distribution", "distribution-verify", "distribution-assets"]);
240
+ for (const name of ["publish", "installers"]) assert.doesNotMatch(all.get(name)!, /distribution/, `${name} is unchanged`);
241
+ assert.match(workflow, /^permissions:\n {2}contents: read\n/m, "the workflow default stays read-only");
242
+ for (const use of workflow.matchAll(/uses: (\S+)/g)) assert.match(use[1], /^[\w-]+\/[\w-]+@[0-9a-f]{40}$/, `${use[1]} is pinned by commit SHA`);
243
+
244
+ const build = all.get("distribution")!;
245
+ assert.match(build, /^ {4}needs: publish$/m, "gentle-pi@<tag> must be on npm before it is resolved");
246
+ assert.match(build, /^ {4}permissions:\n {6}contents: read\n(?! {6})/m);
247
+ assert.match(build, /ref: \$\{\{ github\.sha \}\}/, "built on the verified release commit");
248
+ assert.match(build, /RELEASE_TAG: \$\{\{ needs\.publish\.outputs\.tag \}\}/);
249
+ assert.match(build, /node scripts\/build-distribution\.mjs build --shell "\$\{RELEASE_TAG#v\}" --out "\$\{RUNNER_TEMP\}\/distribution"/);
250
+ assert.match(build, /uses: actions\/upload-artifact@[0-9a-f]{40} # v[\d.]+\n {8}with:\n {10}name: gentle-shell-distribution\n/);
251
+
252
+ const verify = all.get("distribution-verify")!;
253
+ assert.match(verify, /^ {4}needs: distribution$/m);
254
+ assert.match(verify, /^ {4}permissions:\n {6}contents: read\n(?! {6})/m);
255
+ assert.match(verify, /matrix:\n {8}os: \[ubuntu-latest, macos-latest, windows-latest\]\n/);
256
+ assert.match(verify, /fail-fast: false/);
257
+ assert.match(verify, /runs-on: \$\{\{ matrix\.os \}\}/);
258
+ assert.match(verify, /ref: \$\{\{ github\.sha \}\}/);
259
+ assert.match(verify, /uses: actions\/download-artifact@[0-9a-f]{40} # v[\d.]+\n {8}with:\n {10}name: gentle-shell-distribution\n/);
260
+ assert.match(verify, /node scripts\/build-distribution\.mjs verify --assets "\$\{RUNNER_TEMP\}\/distribution"/);
261
+ assert.doesNotMatch(verify, /gh release|contents: write/);
262
+
263
+ const upload = all.get("distribution-assets")!;
264
+ assert.match(upload, /^ {4}needs: \[publish, distribution-verify\]$/m, "uploaded only after every verify lane passed");
265
+ assert.match(upload, /^ {4}permissions:\n {6}contents: write\n(?! {6})/m);
266
+ assert.match(upload, /RELEASE_TAG: \$\{\{ needs\.publish\.outputs\.tag \}\}/);
267
+ assert.match(upload, /gh release upload "\$\{RELEASE_TAG\}" "\$\{RUNNER_TEMP\}\/distribution\/gentle-shell-distribution\.json" "\$\{RUNNER_TEMP\}\/distribution\/gentle-shell-distribution-lock\.yaml" --repo "\$\{GITHUB_REPOSITORY\}" --clobber/);
268
+ for (const [name, job] of all) if (name !== "distribution-assets") assert.doesNotMatch(job, /gentle-shell-distribution\.json/, `${name} never uploads the assets`);
269
+ });
@@ -0,0 +1,242 @@
1
+ import assert from "node:assert/strict";
2
+ import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync, existsSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test, { type TestContext } from "node:test";
6
+ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
7
+ import quietTools from "../extensions/quiet-tools.ts";
8
+ import {
9
+ BUILTIN_CODEMODE_OPTOUT_ENTRY,
10
+ builtinCodemodeOptOutStatePath,
11
+ offerBuiltinCodemodeOptOut,
12
+ withBuiltinExtensionExcluded,
13
+ } from "../lib/builtin-codemode-optout.ts";
14
+
15
+ // gentle-pi replaces Pi's builtin codemode with its compact renderer, so Pi
16
+ // warns at every startup. In a user-owned Pi home gentle-pi may only add the
17
+ // `-builtin:codemode` opt-out after the user accepts a one-time prompt.
18
+
19
+ type Prompt = { title: string; message: string };
20
+
21
+ function fixture(t: TestContext, settings?: string) {
22
+ const root = mkdtempSync(join(tmpdir(), "gentle-pi-codemode-optout-"));
23
+ t.after(() => rmSync(root, { recursive: true, force: true }));
24
+ const agentDir = join(root, "agent");
25
+ const configHome = join(root, "gentle-ai");
26
+ mkdirSync(agentDir, { recursive: true });
27
+ const settingsPath = join(agentDir, "settings.json");
28
+ if (settings !== undefined) writeFileSync(settingsPath, settings);
29
+ return { root, agentDir, configHome, settingsPath };
30
+ }
31
+
32
+ function interactive(answer: boolean, mode: "tui" | "rpc" | "print" | "json" = "tui", hasUI = true) {
33
+ const prompts: Prompt[] = [];
34
+ const notices: string[] = [];
35
+ const ctx = {
36
+ hasUI,
37
+ mode,
38
+ ui: {
39
+ async confirm(title: string, message: string) { prompts.push({ title, message }); return answer; },
40
+ notify(message: string) { notices.push(message); },
41
+ },
42
+ };
43
+ return { ctx, prompts, notices };
44
+ }
45
+
46
+ function extensionHarness(settings: Record<string, unknown> = {}) {
47
+ const tools: ToolDefinition[] = [];
48
+ const handlers = new Map<string, ((event: unknown, ctx: unknown) => unknown)[]>();
49
+ const pi = {
50
+ registerTool(tool: ToolDefinition) { tools.push(tool); },
51
+ on(name: string, handler: (event: unknown, ctx: unknown) => unknown) { handlers.set(name, [...(handlers.get(name) ?? []), handler]); },
52
+ getSettings() { return { codemode: { mode: "on" }, ...settings }; },
53
+ getAllTools() { return []; },
54
+ appendEntry() {},
55
+ } as unknown as ExtensionAPI;
56
+ const startSession = async (ctx: unknown) => {
57
+ for (const handler of handlers.get("session_start") ?? []) handler({ type: "session_start", reason: "startup" }, { sessionManager: {}, ...(ctx as object) });
58
+ // The offer runs detached from session_start; let it settle.
59
+ await new Promise((resolve) => setImmediate(resolve));
60
+ };
61
+ return { pi, tools, startSession };
62
+ }
63
+
64
+ const read = (path: string) => readFileSync(path, "utf8");
65
+
66
+ test("the pure edit appends the opt-out and keeps every other key, indentation, and trailing newline", () => {
67
+ for (const [indent, newline] of [[4, "\n"], ["\t", "\n"], [undefined, ""]] as const) {
68
+ const original = { theme: "rose", extensions: ["./ext/a.ts", "!./ext/b.ts"], packages: ["npm:gentle-pi"] };
69
+ const text = `${JSON.stringify(original, null, indent)}${newline}`;
70
+ const expected = { ...original, extensions: [...original.extensions, BUILTIN_CODEMODE_OPTOUT_ENTRY] };
71
+ assert.equal(withBuiltinExtensionExcluded(text), `${JSON.stringify(expected, null, indent)}${newline}`);
72
+ }
73
+ assert.equal(withBuiltinExtensionExcluded('{"theme":"rose"}'), '{"theme":"rose","extensions":["-builtin:codemode"]}');
74
+ });
75
+
76
+ test("the pure edit refuses explicit entries, malformed JSON, non-object settings, and non-array extensions", () => {
77
+ for (const entry of ["+builtin:codemode", "!builtin:codemode", "builtin:codemode", "-builtin:codemode"]) {
78
+ assert.equal(withBuiltinExtensionExcluded(JSON.stringify({ extensions: [entry] })), undefined, entry);
79
+ }
80
+ for (const text of ["{ not json", "[]", "null", "42", '{"extensions":"oops"}']) {
81
+ assert.equal(withBuiltinExtensionExcluded(text), undefined, text);
82
+ }
83
+ });
84
+
85
+ test("accepting the prompt appends the opt-out to the agent settings.json and says when the warning goes away", async (t) => {
86
+ const original = `${JSON.stringify({ theme: "rose", extensions: ["./ext/a.ts"], packages: ["npm:gentle-pi"] }, null, 2)}\n`;
87
+ const f = fixture(t, original);
88
+ const { ctx, prompts, notices } = interactive(true);
89
+
90
+ const outcome = await offerBuiltinCodemodeOptOut(ctx, { agentDir: f.agentDir, configHome: f.configHome, effectiveExtensions: ["./ext/a.ts"] });
91
+ assert.equal(outcome, "accepted");
92
+ assert.equal(prompts.length, 1);
93
+ assert.match(prompts[0]!.message, /compact/);
94
+ assert.match(prompts[0]!.message, /-builtin:codemode/);
95
+ assert.match(prompts[0]!.message, /Nothing is written unless you accept/);
96
+ assert.ok(prompts[0]!.message.includes(f.settingsPath));
97
+ const expected = { theme: "rose", extensions: ["./ext/a.ts", "-builtin:codemode"], packages: ["npm:gentle-pi"] };
98
+ assert.equal(read(f.settingsPath), `${JSON.stringify(expected, null, 2)}\n`);
99
+ assert.match(notices.join("\n"), /next launch/);
100
+
101
+ // The entry is now explicit: a second session neither prompts nor writes.
102
+ const second = interactive(true);
103
+ assert.equal(await offerBuiltinCodemodeOptOut(second.ctx, { agentDir: f.agentDir, configHome: f.configHome, effectiveExtensions: expected.extensions }), "explicit-entry");
104
+ assert.equal(second.prompts.length, 0);
105
+ assert.equal(read(f.settingsPath), `${JSON.stringify(expected, null, 2)}\n`);
106
+ });
107
+
108
+ test("declining writes nothing to settings.json and is remembered so the next session does not ask", async (t) => {
109
+ const original = '{"theme":"rose"}';
110
+ const f = fixture(t, original);
111
+ const first = interactive(false);
112
+ assert.equal(await offerBuiltinCodemodeOptOut(first.ctx, { agentDir: f.agentDir, configHome: f.configHome }), "declined");
113
+ assert.equal(first.prompts.length, 1);
114
+ assert.equal(read(f.settingsPath), original);
115
+ assert.ok(existsSync(builtinCodemodeOptOutStatePath(f.configHome)));
116
+ assert.match(first.notices.join("\n"), /-builtin:codemode/);
117
+
118
+ const second = interactive(true);
119
+ assert.equal(await offerBuiltinCodemodeOptOut(second.ctx, { agentDir: f.agentDir, configHome: f.configHome }), "declined-before");
120
+ assert.equal(second.prompts.length, 0);
121
+ assert.equal(read(f.settingsPath), original);
122
+ });
123
+
124
+ test("a decline for one agent dir does not silence the prompt for another", async (t) => {
125
+ const a = fixture(t, "{}");
126
+ const b = fixture(t, "{}");
127
+ assert.equal(await offerBuiltinCodemodeOptOut(interactive(false).ctx, { agentDir: a.agentDir, configHome: a.configHome }), "declined");
128
+ const other = interactive(false);
129
+ assert.equal(await offerBuiltinCodemodeOptOut(other.ctx, { agentDir: b.agentDir, configHome: a.configHome }), "declined");
130
+ assert.equal(other.prompts.length, 1);
131
+ });
132
+
133
+ test("any explicit builtin:codemode entry in the file or the effective settings means no prompt and no write", async (t) => {
134
+ for (const entry of ["+builtin:codemode", "!builtin:codemode", "builtin:codemode", "-builtin:codemode"]) {
135
+ const fileText = JSON.stringify({ extensions: ["./ext/a.ts", entry] });
136
+ const inFile = fixture(t, fileText);
137
+ const fromFile = interactive(true);
138
+ assert.equal(await offerBuiltinCodemodeOptOut(fromFile.ctx, { agentDir: inFile.agentDir, configHome: inFile.configHome }), "explicit-entry", entry);
139
+ assert.equal(fromFile.prompts.length, 0);
140
+ assert.equal(read(inFile.settingsPath), fileText);
141
+
142
+ // A project-level entry surfaces only through the effective settings.
143
+ const effective = fixture(t, "{}");
144
+ const fromEffective = interactive(true);
145
+ assert.equal(await offerBuiltinCodemodeOptOut(fromEffective.ctx, { agentDir: effective.agentDir, configHome: effective.configHome, effectiveExtensions: [entry] }), "explicit-entry", entry);
146
+ assert.equal(fromEffective.prompts.length, 0);
147
+ assert.equal(read(effective.settingsPath), "{}");
148
+ }
149
+ });
150
+
151
+ test("malformed, non-object, or non-array-extensions settings are never prompted for or rewritten", async (t) => {
152
+ for (const text of ["{ not json", "[]", '{"extensions":"oops"}']) {
153
+ const f = fixture(t, text);
154
+ const { ctx, prompts } = interactive(true);
155
+ assert.equal(await offerBuiltinCodemodeOptOut(ctx, { agentDir: f.agentDir, configHome: f.configHome }), "unwritable", text);
156
+ assert.equal(prompts.length, 0);
157
+ assert.equal(read(f.settingsPath), text);
158
+ }
159
+ });
160
+
161
+ test("a missing settings.json is not created and no prompt is shown", async (t) => {
162
+ const f = fixture(t);
163
+ const { ctx, prompts } = interactive(true);
164
+ assert.equal(await offerBuiltinCodemodeOptOut(ctx, { agentDir: f.agentDir, configHome: f.configHome }), "unwritable");
165
+ assert.equal(prompts.length, 0);
166
+ assert.equal(existsSync(f.settingsPath), false);
167
+ });
168
+
169
+ test("a symlinked settings.json stays a symlink and its target receives the opt-out", async (t) => {
170
+ const f = fixture(t);
171
+ const dotfiles = join(f.root, "dotfiles");
172
+ mkdirSync(dotfiles);
173
+ const target = join(dotfiles, "pi-settings.json");
174
+ writeFileSync(target, '{\n "theme": "rose"\n}\n');
175
+ symlinkSync(target, f.settingsPath);
176
+
177
+ const { ctx } = interactive(true);
178
+ assert.equal(await offerBuiltinCodemodeOptOut(ctx, { agentDir: f.agentDir, configHome: f.configHome }), "accepted");
179
+ assert.ok(lstatSync(f.settingsPath).isSymbolicLink());
180
+ assert.equal(readlinkSync(f.settingsPath), target);
181
+ assert.equal(read(target), '{\n "theme": "rose",\n "extensions": [\n "-builtin:codemode"\n ]\n}\n');
182
+ });
183
+
184
+ test("without an interactive TUI there is no prompt, no write, and no remembered decline", async (t) => {
185
+ for (const [mode, hasUI] of [["tui", false], ["rpc", true], ["print", false], ["json", false]] as const) {
186
+ const f = fixture(t, "{}");
187
+ const { ctx, prompts } = interactive(true, mode, hasUI);
188
+ assert.equal(await offerBuiltinCodemodeOptOut(ctx, { agentDir: f.agentDir, configHome: f.configHome }), "no-ui", mode);
189
+ assert.equal(prompts.length, 0);
190
+ assert.equal(read(f.settingsPath), "{}");
191
+ assert.equal(existsSync(builtinCodemodeOptOutStatePath(f.configHome)), false);
192
+ }
193
+ });
194
+
195
+ test("a failing dialog never escapes and writes nothing", async (t) => {
196
+ const f = fixture(t, "{}");
197
+ const ctx = { hasUI: true, mode: "tui", ui: { async confirm(): Promise<boolean> { throw new Error("dialog closed"); }, notify() {} } };
198
+ assert.equal(await offerBuiltinCodemodeOptOut(ctx, { agentDir: f.agentDir, configHome: f.configHome }), "failed");
199
+ assert.equal(read(f.settingsPath), "{}");
200
+ });
201
+
202
+ test("quiet-tools always registers the compact codemode and offers the opt-out once per process from session_start", async (t) => {
203
+ const f = fixture(t, "{}");
204
+ const { pi, tools, startSession } = extensionHarness();
205
+ await quietTools(pi, undefined, { agentDir: f.agentDir, configHome: f.configHome });
206
+ assert.ok(tools.some((tool) => tool.name === "codemode"), "compact codemode must stay registered");
207
+
208
+ const declined = interactive(false);
209
+ await startSession(declined.ctx);
210
+ assert.equal(declined.prompts.length, 1);
211
+ assert.equal(read(f.settingsPath), "{}");
212
+
213
+ // A later session_start in the same process (e.g. /new) does not ask again.
214
+ const again = interactive(true);
215
+ await startSession(again.ctx);
216
+ assert.equal(again.prompts.length, 0);
217
+ });
218
+
219
+ test("quiet-tools reads explicit entries from the effective settings and stays silent without a TUI", async (t) => {
220
+ for (const [settings, session] of [
221
+ [{ extensions: ["+builtin:codemode"] }, interactive(true)],
222
+ [{}, interactive(true, "rpc")],
223
+ [{}, interactive(true, "tui", false)],
224
+ ] as const) {
225
+ const f = fixture(t, "{}");
226
+ const { pi, tools, startSession } = extensionHarness(settings);
227
+ await quietTools(pi, undefined, { agentDir: f.agentDir, configHome: f.configHome });
228
+ assert.ok(tools.some((tool) => tool.name === "codemode"));
229
+ await startSession(session.ctx);
230
+ assert.equal(session.prompts.length, 0);
231
+ assert.equal(read(f.settingsPath), "{}");
232
+ }
233
+ });
234
+
235
+ test("quiet-tools accepts the opt-out end to end", async (t) => {
236
+ const f = fixture(t, '{"theme":"rose"}\n');
237
+ const { pi, tools, startSession } = extensionHarness();
238
+ await quietTools(pi, undefined, { agentDir: f.agentDir, configHome: f.configHome });
239
+ assert.ok(tools.some((tool) => tool.name === "codemode"));
240
+ await startSession(interactive(true).ctx);
241
+ assert.equal(read(f.settingsPath), '{"theme":"rose","extensions":["-builtin:codemode"]}\n');
242
+ });