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,220 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, isAbsolute, parse, relative, resolve } from "node:path";
4
+ import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktree-registry.ts";
5
+
6
+ const WRITER_NAMES = ["gentle-ai-worker", "worker", "jd-fix-agent"];
7
+ export const WRITER_EDIT_SURFACE_REJECTION =
8
+ "Writer tasks must include the exact Markdown heading `## Allowed edit surfaces` with narrow repository-relative paths or narrow globs, one per line. Every non-empty line belongs to the section until the next canonical Markdown heading and must be a valid surface entry. Paths containing whitespace require whole-entry backticks; begin explanatory prose under the next Markdown heading. The parent must derive or map that canonical block from the delegated task and relaunch the writer; do not accept aliases, and do not ask the human to author paths or globs.";
9
+ // One heading matcher: the parser scans with the global form, and continuation
10
+ // inheritance tests presence with the same source (review R3-003).
11
+ const ALLOWED_EDIT_SURFACES_HEADING_LINE = /^## Allowed edit surfaces[ \t]*$/im;
12
+ const ALLOWED_EDIT_SURFACES_HEADING = new RegExp(ALLOWED_EDIT_SURFACES_HEADING_LINE.source, "gim");
13
+ const MARKDOWN_HEADING_LINE = /^ {0,3}#{1,6} /;
14
+ const MARKDOWN_LIST_MARKER = /^(?:[-*+]|\d+[.)]) +/;
15
+
16
+ function isTaskScopedRepositoryRelativePath(value: string, backticked: boolean): boolean {
17
+ const normalized = value.replace(/\\/g, "/");
18
+ if (!normalized || isAbsolute(value) || /^(?:[A-Za-z]:|\/|~)/.test(normalized) || /\p{Cc}|\p{Zl}|\p{Zp}/u.test(normalized) || (/\p{White_Space}/u.test(normalized) && !backticked)) return false;
19
+ const path = normalized.replace(/^(?:\.\/)+/, "");
20
+ if (!path || path === "." || path.startsWith("/") || path.split("/").some(segment => segment === "..")) return false;
21
+ return !/[?*\[\]{}]/.test(path.split("/")[0]);
22
+ }
23
+
24
+ // This is the same canonical parser used by both the tool hook and executor.
25
+ // Prose cannot close the section, and repeated sections must agree exactly.
26
+ // A rejection carries the concrete problem so the caller repairs only the
27
+ // section instead of re-summarizing the whole task (gentle-shell#1713).
28
+ function parseAllowedEditSurfaces(values: readonly unknown[]): { paths: string[] } | { problem: string } {
29
+ let expected: string[] | undefined;
30
+ for (const value of values) {
31
+ if (typeof value !== "string") continue;
32
+ for (const heading of value.matchAll(ALLOWED_EDIT_SURFACES_HEADING)) {
33
+ const lines = value.slice((heading.index ?? 0) + heading[0].length).split(/\r?\n/);
34
+ const end = lines.findIndex(line => MARKDOWN_HEADING_LINE.test(line));
35
+ const entries = (end === -1 ? lines : lines.slice(0, end)).map(line => line.replace(/ +$/g, "")).filter(Boolean);
36
+ if (!entries.length) return { problem: "The section has no entries." };
37
+ const paths: string[] = [];
38
+ for (const source of entries) {
39
+ const line = source.replace(/^ {0,3}/, "");
40
+ const unlisted = line.replace(MARKDOWN_LIST_MARKER, "");
41
+ const quoted = unlisted.match(/^`([^`]+)`$/);
42
+ const path = quoted?.[1] ?? unlisted;
43
+ if (/^(?:[-*+]|\d+[.)])$/.test(line) || (unlisted.includes("`") && !quoted) || /\p{Cc}|\p{Zl}|\p{Zp}/u.test(source) || !isTaskScopedRepositoryRelativePath(path, !!quoted)) {
44
+ // Prose and a bad path need different repairs: moving a real surface
45
+ // out of the section would silently narrow the writer's scope.
46
+ // An unquoted entry with whitespace that would be valid when quoted is
47
+ // ambiguous: name both repairs instead of guessing prose.
48
+ const prose = /^(?:[-*+]|\d+[.)])$/.test(line) || (!quoted && /\p{White_Space}/u.test(unlisted));
49
+ const quotable = prose && !quoted && !unlisted.includes("`") && !/\p{Cc}|\p{Zl}|\p{Zp}/u.test(source) && isTaskScopedRepositoryRelativePath(unlisted, true);
50
+ const shown = (prose ? source.trim() : path).replace(/\p{Cc}|\p{Zl}|\p{Zp}/gu, " ");
51
+ const bounded = shown.length > 120 ? `${shown.slice(0, 120)}...` : shown;
52
+ return { problem: quotable
53
+ ? `Line "${bounded}" is not a valid surface entry; if it is a path, wrap the whole entry in backticks, otherwise move it under a following Markdown heading.`
54
+ : prose
55
+ ? `Line "${bounded}" is not a valid surface entry; move prose under a following Markdown heading.`
56
+ : `Entry "${bounded}" is not a narrow repository-relative path; remove absolute paths, \`..\` segments, root globs, and stray backticks.` };
57
+ }
58
+ paths.push(path);
59
+ }
60
+ const unique = [...new Set(paths)].sort();
61
+ if (expected && (expected.length !== unique.length || expected.some((path, index) => path !== unique[index]))) return { problem: "Repeated sections list different surfaces." };
62
+ expected = unique;
63
+ }
64
+ }
65
+ return expected ? { paths: expected } : { problem: "No `## Allowed edit surfaces` heading was found." };
66
+ }
67
+
68
+ export function allowedEditSurfaces(...values: unknown[]): string[] | undefined {
69
+ const parsed = parseAllowedEditSurfaces(values);
70
+ return "paths" in parsed ? parsed.paths : undefined;
71
+ }
72
+
73
+ export function rejectUnscopedBoundedWriterDispatch(input: unknown): { block: true; reason: string } | undefined {
74
+ if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
75
+ const record = input as Record<string, unknown>;
76
+ if (typeof record.agent !== "string" || !WRITER_NAMES.includes(record.agent)) return undefined;
77
+ const parsed = parseAllowedEditSurfaces([record.task, record.context]);
78
+ if ("paths" in parsed) return undefined;
79
+ return { block: true, reason: `${WRITER_EDIT_SURFACE_REJECTION} ${parsed.problem} Resend the same task text unchanged except for that section; never shorten or re-summarize it.` };
80
+ }
81
+
82
+ // A continuation resumes the same delegated task, so a writer follow-up that
83
+ // carries no section of its own inherits the surfaces its original launch was
84
+ // admitted with. A follow-up that carries the heading is validated as written,
85
+ // never merged, and nothing is inherited when the original had no valid section.
86
+ // Judgment Day fix batches keep their own exact dispatch protocol.
87
+ export function inheritAllowedEditSurfaces(agent: string, followUp: string, context: unknown, originalPrompt: string): string {
88
+ const hasHeading = (value: unknown) => typeof value === "string" && ALLOWED_EDIT_SURFACES_HEADING_LINE.test(value);
89
+ if (!isGenericBoundedWriter(agent) || hasHeading(followUp) || hasHeading(context)) return followUp;
90
+ const inherited = allowedEditSurfaces(originalPrompt);
91
+ if (!inherited) return followUp;
92
+ // Every inherited entry is backticked: some entries are admitted only when
93
+ // quoted, and quoting never changes a valid path (review R3-002).
94
+ return `${followUp}\n\n## Allowed edit surfaces\n${inherited.map(path => `\`${path}\``).join("\n")}\n`;
95
+ }
96
+
97
+ // Every agent admitted through the `## Allowed edit surfaces` guard, including
98
+ // Judgment Day fix agents; only these claim surfaces at runtime admission.
99
+ export function isBoundedWriter(name: string): boolean {
100
+ return WRITER_NAMES.includes(name);
101
+ }
102
+
103
+ export function isGenericBoundedWriter(name: string): boolean {
104
+ return name === "gentle-ai-worker" || name === "worker";
105
+ }
106
+
107
+ // Source authorization is structural, never inferred from task prose. Deliberately
108
+ // exclude documentation, memory, harness metadata and configuration-only scopes.
109
+ export function isDevelopmentSurface(path: string): boolean {
110
+ const normalized = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
111
+ if (normalized.split("/").some(part => /^(?:\.|\.\.|odd|docs?|memory|secrets|credentials|tokens|keychains|\.pi|\.agents|\.atl|\.git|\.config|\.ssh|\.aws|\.gnupg|\.credentials)$/i.test(part))) return false;
112
+ if (/(^|\/)\w[\w.-]*\.config\.[cm]?[jt]s$/i.test(normalized)) return false;
113
+ if (/(^|\/)\.env(?:$|[./_-])|\.(?:pem|key|p12|pfx)$/i.test(normalized)) return false;
114
+ return /\.(?:[cm]?[jt]sx?|go|rs|py|java|kt|swift|c|cc|cpp|h|hpp|cs|rb|php|vue|svelte|html|css|scss|sass|sql|sh)$/i.test(normalized) || /^(?:src|lib|tests?|app|packages)\/(?:[^\s]+\/)*\*\*?(?:\/\*)?$/.test(normalized);
115
+ }
116
+
117
+ // Broad folders a user opens by accident. A repository there (or in HOME, or
118
+ // above it) makes every Git call scan the user's whole tree.
119
+ const BROAD_HOME_FOLDER = /^(?:documents|desktop|downloads|onedrive(?: - .+)?)$/i;
120
+ const BROAD_ONEDRIVE_FOLDER = /^(?:documents|desktop)$/i;
121
+
122
+ export interface BootstrapDirectoryOptions {
123
+ /** Defaults to the canonical home directory. */
124
+ readonly home?: string;
125
+ /** Defaults to `process.platform`; Windows paths compare case-insensitively. */
126
+ readonly platform?: NodeJS.Platform;
127
+ }
128
+
129
+ /** True when `path` is HOME, an ancestor of it, or a broad folder directly under it. */
130
+ export function isHomeOrBroadUserDirectory(path: string, options: BootstrapDirectoryOptions = {}): boolean {
131
+ const platform = options.platform ?? process.platform;
132
+ const fold = (value: string) => platform === "win32" ? value.toLowerCase() : value;
133
+ let home: string;
134
+ try { home = options.home ?? realpathSync.native(homedir()); } catch { return true; }
135
+ const target = fold(path), canonicalHome = fold(home);
136
+ const towardHome = relative(target, canonicalHome);
137
+ if (towardHome === "" || (!isAbsolute(towardHome) && towardHome !== ".." && !towardHome.startsWith("..\\") && !towardHome.startsWith("../"))) return true;
138
+ const parent = dirname(target);
139
+ if (parent === canonicalHome) return BROAD_HOME_FOLDER.test(basename(path));
140
+ return dirname(parent) === canonicalHome && /^onedrive(?: - .+)?$/i.test(basename(parent)) && BROAD_ONEDRIVE_FOLDER.test(basename(path));
141
+ }
142
+
143
+ export function safeBootstrapDirectory(path: string, options: BootstrapDirectoryOptions = {}): string | undefined {
144
+ try {
145
+ if (!isAbsolute(path)) return undefined;
146
+ const root = realpathSync(path);
147
+ if (!statSync(root).isDirectory() || root === parse(root).root || isHomeOrBroadUserDirectory(realpathSync.native(root), options)) return undefined;
148
+ if (/(^|[\\/])(?:\.ssh|\.credentials|\.aws|\.gnupg|\.config|secrets|credentials|tokens|keychains)(?:[\\/]|$)|(?:^|[\\/])\.env(?:$|[./_-])/i.test(root)) return undefined;
149
+ return root;
150
+ } catch { return undefined; }
151
+ }
152
+
153
+ export function sourcePathWithinProject(path: string, cwd: string): string | undefined {
154
+ const root = safeBootstrapDirectory(cwd);
155
+ if (!root) return undefined;
156
+ try {
157
+ const file = realpathSync(resolve(cwd, path.replace(/^@/, "").replace(/^~(?=\/|$)/, homedir())));
158
+ const within = relative(root, file);
159
+ if (isAbsolute(within) || within === ".." || within.startsWith("../") || within.startsWith("..\\") || !isDevelopmentSurface(within)) return undefined;
160
+ return root;
161
+ } catch { return undefined; }
162
+ }
163
+
164
+ // Capture synchronously before startup's first await. Once a clone is observed,
165
+ // its absence or replacement is drift, never permission to initialize again.
166
+ export function sessionRepositoryAuthority(cwd: string, resolver: WorktreeResolver = resolveSessionWorktree): () => boolean {
167
+ const initial = resolver(cwd, cwd);
168
+ let established = initial && { ...initial };
169
+ return () => {
170
+ const current = resolver(cwd, cwd);
171
+ if (established) return current?.root === established.root && current?.commonDir === established.commonDir;
172
+ if (current) established = { ...current };
173
+ return true;
174
+ };
175
+ }
176
+
177
+ interface SessionOwner { getSessionId(): string; getCwd?(): string }
178
+ type Preparation = (root: string, current: () => boolean, signal?: AbortSignal) => Promise<boolean>;
179
+ interface Binding { id: string; cwd: string; prepare: Preparation; current(): boolean; pending?: Promise<boolean> }
180
+ const preparations = new WeakMap<SessionOwner, Binding>();
181
+ const preparationOwners = new WeakSet<SessionOwner>();
182
+
183
+ // Session-bound callback, not a target authorization cache. Native policy and
184
+ // metadata checks remain in the native extension. Unbind only our incarnation.
185
+ export function bindSessionRepositoryPreparation(manager: SessionOwner, cwd: string, prepare: Preparation, live: () => boolean): () => void {
186
+ const authorityCurrent = sessionRepositoryAuthority(cwd);
187
+ const binding: Binding = { id: manager.getSessionId(), cwd, prepare, current: () => live() && manager.getSessionId() === binding.id && (manager.getCwd?.() ?? cwd) === cwd && authorityCurrent() };
188
+ preparationOwners.add(manager);
189
+ preparations.set(manager, binding);
190
+ return () => { if (preparations.get(manager) === binding) preparations.delete(manager); };
191
+ }
192
+
193
+ // Pin the exact incarnation before awaiting native mode. An absent binding is
194
+ // compatible only for never-bound callers, not a previously revoked owner.
195
+ export function captureBoundSessionRepositoryAuthority(manager: SessionOwner, cwd: string): () => boolean {
196
+ const binding = preparations.get(manager);
197
+ const id = manager.getSessionId();
198
+ return () => manager.getSessionId() === id && (manager.getCwd?.() ?? cwd) === cwd &&
199
+ preparations.get(manager) === binding && (binding
200
+ ? binding.cwd === cwd && binding.current()
201
+ : !preparationOwners.has(manager));
202
+ }
203
+
204
+ export function boundSessionRepositoryAuthorityCurrent(manager: SessionOwner, cwd: string): boolean {
205
+ return captureBoundSessionRepositoryAuthority(manager, cwd)();
206
+ }
207
+
208
+ export async function prepareBoundSessionRepository(manager: SessionOwner, cwd: string, signal?: AbortSignal): Promise<boolean> {
209
+ const binding = preparations.get(manager);
210
+ const current = () => preparations.get(manager) === binding && !!binding?.current() && !signal?.aborted;
211
+ if (!binding || binding.cwd !== cwd || !current()) return false;
212
+ const root = safeBootstrapDirectory(cwd);
213
+ if (!root) return false;
214
+ if (!binding.pending) {
215
+ binding.pending = binding.prepare(root, current, signal);
216
+ binding.pending.finally(() => { binding.pending = undefined; }).catch(() => {});
217
+ }
218
+ try { return await binding.pending && current() && safeBootstrapDirectory(cwd) === root; }
219
+ catch { return false; }
220
+ }
@@ -0,0 +1,166 @@
1
+ import { chmodSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import { gentlePiConfigHome } from "./agent-home.ts";
6
+
7
+ // gentle-pi replaces Pi's replaceable builtin codemode with its compact
8
+ // renderer (lib/codemode-renderer.ts), and Pi warns at every startup when a
9
+ // builtin loses its tool to another extension. Pi's only per-builtin opt-out
10
+ // is a `-builtin:<name>` entry in the settings `extensions` array. The
11
+ // gentle-shell launcher adds it silently to homes it owns (bin/gentle-shell.mjs,
12
+ // #1612); a user-owned Pi home only gets it after the user accepts a
13
+ // one-time prompt, and a declined prompt is remembered per settings file.
14
+ export const BUILTIN_CODEMODE_EXTENSION = "builtin:codemode";
15
+ export const BUILTIN_CODEMODE_OPTOUT_ENTRY = `-${BUILTIN_CODEMODE_EXTENSION}`;
16
+ export const BUILTIN_CODEMODE_OPTOUT_SCHEMA = "gentle-pi.builtin-codemode-optout/v1";
17
+
18
+ export type BuiltinCodemodeOptOutOutcome =
19
+ | "no-ui"
20
+ | "explicit-entry"
21
+ | "unwritable"
22
+ | "declined-before"
23
+ | "accepted"
24
+ | "declined"
25
+ | "failed";
26
+
27
+ export interface BuiltinCodemodeOptOutOptions {
28
+ /** Pi agent dir whose settings.json receives the entry; defaults to getAgentDir(). */
29
+ agentDir?: string;
30
+ /** gentle-pi config home holding the remembered decline; defaults to gentlePiConfigHome(). */
31
+ configHome?: string;
32
+ /** Effective (merged) settings `extensions`, e.g. from pi.getSettings(). */
33
+ effectiveExtensions?: unknown;
34
+ }
35
+
36
+ export interface BuiltinCodemodeOptOutContext {
37
+ hasUI: boolean;
38
+ mode: string;
39
+ ui: {
40
+ confirm(title: string, message: string): Promise<boolean>;
41
+ notify(message: string, type?: "info" | "warning" | "error"): void;
42
+ };
43
+ }
44
+
45
+ /** True when `extensions` already holds any explicit (`+`, `-`, `!`, or bare) entry for the builtin. */
46
+ export function hasExplicitBuiltinEntry(extensions: unknown, builtin = BUILTIN_CODEMODE_EXTENSION): boolean {
47
+ return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string" && entry.replace(/^[+!-]/, "") === builtin);
48
+ }
49
+
50
+ // Pure: mirrors withBuiltinExtensionExcluded in bin/gentle-shell.mjs. Returns
51
+ // `settingsText` with `-<builtin>` appended to its `extensions` array (created
52
+ // when absent), or undefined when nothing should change — the text does not
53
+ // parse as a JSON object, `extensions` exists but is not an array, or the
54
+ // array already holds an explicit entry for the builtin, which is the user's
55
+ // own decision. Keeps every other key and entry, the text's indentation, and
56
+ // its trailing newline.
57
+ export function withBuiltinExtensionExcluded(settingsText: string, builtin = BUILTIN_CODEMODE_EXTENSION): string | undefined {
58
+ let settings: unknown;
59
+ try {
60
+ settings = JSON.parse(settingsText);
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return undefined;
65
+ const extensions = Object.prototype.hasOwnProperty.call(settings, "extensions") ? (settings as { extensions: unknown }).extensions : [];
66
+ if (!Array.isArray(extensions) || hasExplicitBuiltinEntry(extensions, builtin)) return undefined;
67
+ const indent = settingsText.match(/\{\r?\n([ \t]+)/)?.[1];
68
+ const serialized = JSON.stringify({ ...settings, extensions: [...extensions, `-${builtin}`] }, null, indent);
69
+ return settingsText.endsWith("\n") ? `${serialized}\n` : serialized;
70
+ }
71
+
72
+ export function builtinCodemodeOptOutStatePath(configHome = gentlePiConfigHome()): string {
73
+ return join(configHome, "builtin-codemode-optout.json");
74
+ }
75
+
76
+ function readIfExists(path: string): string | undefined {
77
+ try {
78
+ return readFileSync(path, "utf8");
79
+ } catch (error) {
80
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ function readDeclined(configHome: string): string[] {
86
+ try {
87
+ const value: unknown = JSON.parse(readIfExists(builtinCodemodeOptOutStatePath(configHome)) ?? "null");
88
+ if (typeof value !== "object" || value === null || (value as { schema?: unknown }).schema !== BUILTIN_CODEMODE_OPTOUT_SCHEMA) return [];
89
+ const declined = (value as { declined?: unknown }).declined;
90
+ return Array.isArray(declined) ? declined.filter((path): path is string => typeof path === "string") : [];
91
+ } catch { return []; }
92
+ }
93
+
94
+ /** Same-directory rename prevents readers from seeing a partially written state file. */
95
+ function recordDeclined(configHome: string, settingsPath: string): void {
96
+ const declined = readDeclined(configHome);
97
+ if (declined.includes(settingsPath)) return;
98
+ mkdirSync(configHome, { recursive: true });
99
+ const path = builtinCodemodeOptOutStatePath(configHome);
100
+ const temporary = `${path}.${randomUUID()}.tmp`;
101
+ try {
102
+ writeFileSync(temporary, `${JSON.stringify({ schema: BUILTIN_CODEMODE_OPTOUT_SCHEMA, declined: [...declined, settingsPath] })}\n`, { flag: "wx", mode: 0o600 });
103
+ renameSync(temporary, path);
104
+ } finally {
105
+ try { unlinkSync(temporary); } catch { /* Rename already consumed the temporary file. */ }
106
+ }
107
+ }
108
+
109
+ // Writes through a symlinked settings.json to its real target, so the link
110
+ // stays a link, with the same temp-file-then-rename and file mode as the
111
+ // launcher's writeFileAtomically.
112
+ function writeSettingsAtomically(path: string, data: string): void {
113
+ const target = realpathSync(path);
114
+ const mode = statSync(target).mode & 0o777;
115
+ const temporary = join(dirname(target), `.${basename(target)}.gentle-pi-${randomUUID()}.tmp`);
116
+ try {
117
+ writeFileSync(temporary, data, { flag: "wx" });
118
+ chmodSync(temporary, mode);
119
+ renameSync(temporary, target);
120
+ } finally {
121
+ try { unlinkSync(temporary); } catch { /* Rename already consumed the temporary file. */ }
122
+ }
123
+ }
124
+
125
+ function promptMessage(settingsPath: string): string {
126
+ return [
127
+ "Pi warns at startup that the builtin codemode extension was not loaded, because gentle-pi replaces it with its compact codemode renderer.",
128
+ `Adding "${BUILTIN_CODEMODE_OPTOUT_ENTRY}" to the "extensions" list in ${settingsPath} silences that warning from the next launch. gentle-pi keeps its compact codemode either way.`,
129
+ "Nothing is written unless you accept. If you decline, gentle-pi will not ask again.",
130
+ ].join("\n\n");
131
+ }
132
+
133
+ // Asks once, in an interactive TUI only, whether to add the opt-out to the
134
+ // agent settings.json, and writes only on acceptance. A missing settings.json
135
+ // is never created: Pi and `pi install` own that file. Never throws.
136
+ export async function offerBuiltinCodemodeOptOut(ctx: BuiltinCodemodeOptOutContext, options: BuiltinCodemodeOptOutOptions = {}): Promise<BuiltinCodemodeOptOutOutcome> {
137
+ try {
138
+ if (!ctx.hasUI || ctx.mode !== "tui") return "no-ui";
139
+ if (hasExplicitBuiltinEntry(options.effectiveExtensions)) return "explicit-entry";
140
+ const settingsPath = resolve(options.agentDir ?? getAgentDir(), "settings.json");
141
+ const configHome = options.configHome ?? gentlePiConfigHome();
142
+ const currentText = readIfExists(settingsPath);
143
+ if (currentText === undefined) return "unwritable";
144
+ try {
145
+ if (hasExplicitBuiltinEntry((JSON.parse(currentText) as { extensions?: unknown } | null)?.extensions)) return "explicit-entry";
146
+ } catch { /* Malformed JSON is reported as unwritable below. */ }
147
+ if (withBuiltinExtensionExcluded(currentText) === undefined) return "unwritable";
148
+ if (readDeclined(configHome).includes(settingsPath)) return "declined-before";
149
+
150
+ const accepted = (await ctx.ui.confirm("Silence Pi's builtin codemode warning?", promptMessage(settingsPath))) === true;
151
+ if (!accepted) {
152
+ recordDeclined(configHome, settingsPath);
153
+ ctx.ui.notify(`gentle-pi will not ask again. To silence the warning later, add "${BUILTIN_CODEMODE_OPTOUT_ENTRY}" to "extensions" in ${settingsPath}.`, "info");
154
+ return "declined";
155
+ }
156
+ // The dialog may have stayed open while the file changed; re-read it.
157
+ const freshText = readIfExists(settingsPath);
158
+ const newText = freshText === undefined ? undefined : withBuiltinExtensionExcluded(freshText);
159
+ if (newText === undefined) return "unwritable";
160
+ writeSettingsAtomically(settingsPath, newText);
161
+ ctx.ui.notify(`Added "${BUILTIN_CODEMODE_OPTOUT_ENTRY}" to ${settingsPath}. The builtin codemode warning disappears from the next launch.`, "info");
162
+ return "accepted";
163
+ } catch {
164
+ return "failed";
165
+ }
166
+ }
@@ -0,0 +1,85 @@
1
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { gentlePiConfigHome } from "./agent-home.ts";
5
+
6
+ // The conversation card content level chosen in Gentle → Customize. `default`
7
+ // keeps the quiet tools' result previews; `minimal` draws every quiet tool
8
+ // card as the command alone. A missing file means default, so nobody loses the
9
+ // previews without choosing to; a malformed or unreadable one also reads as
10
+ // default, and the writer refuses
11
+ // to replace it so a hand edit is never lost. There is no environment override.
12
+ export const CARD_CONTENT_SCHEMA = "gentle-pi.card-content/v1";
13
+ const CARD_CONTENT_FILE = "card-content.json";
14
+
15
+ export const CARD_CONTENT = {
16
+ DEFAULT: "default",
17
+ MINIMAL: "minimal",
18
+ } as const;
19
+
20
+ export type CardContent = (typeof CARD_CONTENT)[keyof typeof CARD_CONTENT];
21
+
22
+ interface CardContentOptions { gentlePiConfigHome?: string }
23
+
24
+ export interface CardContentResolution {
25
+ content: CardContent;
26
+ source: "global_file" | "default";
27
+ malformed: boolean;
28
+ globalFile: string;
29
+ }
30
+
31
+ function isCardContent(value: unknown): value is CardContent {
32
+ return value === CARD_CONTENT.DEFAULT || value === CARD_CONTENT.MINIMAL;
33
+ }
34
+
35
+ export function parseCardContentFile(raw: string): CardContent | undefined {
36
+ try {
37
+ const value: unknown = JSON.parse(raw);
38
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
39
+ if (Object.keys(value).length !== 2 || !("schema" in value) || value.schema !== CARD_CONTENT_SCHEMA || !("content" in value)) return undefined;
40
+ return isCardContent(value.content) ? value.content : undefined;
41
+ } catch { return undefined; }
42
+ }
43
+
44
+ export function resolveCardContent(options: CardContentOptions = {}): CardContentResolution {
45
+ const globalFile = join(options.gentlePiConfigHome ?? gentlePiConfigHome(), CARD_CONTENT_FILE);
46
+ try {
47
+ const content = parseCardContentFile(readFileSync(globalFile, "utf8"));
48
+ return { content: content ?? CARD_CONTENT.DEFAULT, source: "global_file", malformed: content === undefined, globalFile };
49
+ } catch (error) {
50
+ const missing = typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
51
+ return { content: CARD_CONTENT.DEFAULT, source: missing ? "default" : "global_file", malformed: !missing, globalFile };
52
+ }
53
+ }
54
+
55
+ export function writeCardContent(content: CardContent, options: CardContentOptions = {}): string {
56
+ if (!isCardContent(content)) throw new TypeError("Invalid card content");
57
+ const home = options.gentlePiConfigHome ?? gentlePiConfigHome();
58
+ const current = resolveCardContent({ gentlePiConfigHome: home });
59
+ if (current.malformed) throw new Error(`Cannot update malformed or unreadable card content preference: ${current.globalFile}`);
60
+ const path = current.globalFile;
61
+ const temporary = `${path}.${randomUUID()}.tmp`;
62
+ mkdirSync(home, { recursive: true });
63
+ try {
64
+ writeFileSync(temporary, `${JSON.stringify({ schema: CARD_CONTENT_SCHEMA, content })}\n`, { flag: "wx", mode: 0o600 });
65
+ renameSync(temporary, path);
66
+ } finally {
67
+ try { unlinkSync(temporary); } catch { /* Rename consumed the temporary file. */ }
68
+ }
69
+ return path;
70
+ }
71
+
72
+ // Pi loads every extension with its own module cache, so the renderers in
73
+ // quiet-tools and the customize command in gentle-shell never share a module
74
+ // instance. The process-wide slot keeps one live content level per process,
75
+ // exactly like the card style slot; an unset slot reads as default.
76
+ const CARD_CONTENT_SLOT = Symbol.for("gentle-pi.card-content");
77
+ const contentState = globalThis as typeof globalThis & { [CARD_CONTENT_SLOT]?: unknown };
78
+
79
+ export function cardContent(): CardContent {
80
+ return contentState[CARD_CONTENT_SLOT] === CARD_CONTENT.MINIMAL ? CARD_CONTENT.MINIMAL : CARD_CONTENT.DEFAULT;
81
+ }
82
+
83
+ export function setCardContent(content: CardContent): void {
84
+ contentState[CARD_CONTENT_SLOT] = content;
85
+ }
@@ -0,0 +1,60 @@
1
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { gentlePiConfigHome } from "./agent-home.ts";
5
+ import { CARD_STYLE, type CardStyle } from "./shell-card.ts";
6
+
7
+ // The conversation card style chosen in Gentle → Customize. A missing file
8
+ // means the borderless float style; a malformed or unreadable one also reads
9
+ // as float, and the writer refuses to replace it so a hand edit is never lost.
10
+ // There is no environment override.
11
+ export const CARD_STYLE_SCHEMA = "gentle-pi.card-style/v1";
12
+ const CARD_STYLE_FILE = "card-style.json";
13
+ interface CardStyleOptions { gentlePiConfigHome?: string }
14
+ export interface CardStyleResolution {
15
+ style: CardStyle;
16
+ source: "global_file" | "default";
17
+ malformed: boolean;
18
+ globalFile: string;
19
+ }
20
+
21
+ function isCardStyle(value: unknown): value is CardStyle {
22
+ return value === CARD_STYLE.NEON || value === CARD_STYLE.FLOAT;
23
+ }
24
+
25
+ export function parseCardStyleFile(raw: string): CardStyle | undefined {
26
+ try {
27
+ const value: unknown = JSON.parse(raw);
28
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
29
+ if (Object.keys(value).length !== 2 || !("schema" in value) || value.schema !== CARD_STYLE_SCHEMA || !("style" in value)) return undefined;
30
+ return isCardStyle(value.style) ? value.style : undefined;
31
+ } catch { return undefined; }
32
+ }
33
+
34
+ export function resolveCardStyle(options: CardStyleOptions = {}): CardStyleResolution {
35
+ const globalFile = join(options.gentlePiConfigHome ?? gentlePiConfigHome(), CARD_STYLE_FILE);
36
+ try {
37
+ const style = parseCardStyleFile(readFileSync(globalFile, "utf8"));
38
+ return { style: style ?? CARD_STYLE.FLOAT, source: "global_file", malformed: style === undefined, globalFile };
39
+ } catch (error) {
40
+ const missing = typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
41
+ return { style: CARD_STYLE.FLOAT, source: missing ? "default" : "global_file", malformed: !missing, globalFile };
42
+ }
43
+ }
44
+
45
+ export function writeCardStyle(style: CardStyle, options: CardStyleOptions = {}): string {
46
+ if (!isCardStyle(style)) throw new TypeError("Invalid card style");
47
+ const home = options.gentlePiConfigHome ?? gentlePiConfigHome();
48
+ const current = resolveCardStyle({ gentlePiConfigHome: home });
49
+ if (current.malformed) throw new Error(`Cannot update malformed or unreadable card style preference: ${current.globalFile}`);
50
+ const path = current.globalFile;
51
+ const temporary = `${path}.${randomUUID()}.tmp`;
52
+ mkdirSync(home, { recursive: true });
53
+ try {
54
+ writeFileSync(temporary, `${JSON.stringify({ schema: CARD_STYLE_SCHEMA, style })}\n`, { flag: "wx", mode: 0o600 });
55
+ renameSync(temporary, path);
56
+ } finally {
57
+ try { unlinkSync(temporary); } catch { /* Rename consumed the temporary file. */ }
58
+ }
59
+ return path;
60
+ }