gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,12 +1,15 @@
1
1
  import { realpathSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { isAbsolute, parse, relative, resolve } from "node:path";
3
+ import { basename, dirname, isAbsolute, parse, relative, resolve } from "node:path";
4
4
  import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktree-registry.ts";
5
5
 
6
6
  const WRITER_NAMES = ["gentle-ai-worker", "worker", "jd-fix-agent"];
7
7
  export const WRITER_EDIT_SURFACE_REJECTION =
8
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
- const ALLOWED_EDIT_SURFACES_HEADING = /^## Allowed edit surfaces[ \t]*$/gim;
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");
10
13
  const MARKDOWN_HEADING_LINE = /^ {0,3}#{1,6} /;
11
14
  const MARKDOWN_LIST_MARKER = /^(?:[-*+]|\d+[.)]) +/;
12
15
 
@@ -20,7 +23,9 @@ function isTaskScopedRepositoryRelativePath(value: string, backticked: boolean):
20
23
 
21
24
  // This is the same canonical parser used by both the tool hook and executor.
22
25
  // Prose cannot close the section, and repeated sections must agree exactly.
23
- export function allowedEditSurfaces(...values: unknown[]): string[] | undefined {
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 } {
24
29
  let expected: string[] | undefined;
25
30
  for (const value of values) {
26
31
  if (typeof value !== "string") continue;
@@ -28,29 +33,71 @@ export function allowedEditSurfaces(...values: unknown[]): string[] | undefined
28
33
  const lines = value.slice((heading.index ?? 0) + heading[0].length).split(/\r?\n/);
29
34
  const end = lines.findIndex(line => MARKDOWN_HEADING_LINE.test(line));
30
35
  const entries = (end === -1 ? lines : lines.slice(0, end)).map(line => line.replace(/ +$/g, "")).filter(Boolean);
31
- if (!entries.length) return undefined;
36
+ if (!entries.length) return { problem: "The section has no entries." };
32
37
  const paths: string[] = [];
33
38
  for (const source of entries) {
34
39
  const line = source.replace(/^ {0,3}/, "");
35
40
  const unlisted = line.replace(MARKDOWN_LIST_MARKER, "");
36
41
  const quoted = unlisted.match(/^`([^`]+)`$/);
37
42
  const path = quoted?.[1] ?? unlisted;
38
- if (/^(?:[-*+]|\d+[.)])$/.test(line) || (unlisted.includes("`") && !quoted) || /\p{Cc}|\p{Zl}|\p{Zp}/u.test(source) || !isTaskScopedRepositoryRelativePath(path, !!quoted)) return undefined;
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
+ }
39
58
  paths.push(path);
40
59
  }
41
60
  const unique = [...new Set(paths)].sort();
42
- if (expected && (expected.length !== unique.length || expected.some((path, index) => path !== unique[index]))) return undefined;
61
+ if (expected && (expected.length !== unique.length || expected.some((path, index) => path !== unique[index]))) return { problem: "Repeated sections list different surfaces." };
43
62
  expected = unique;
44
63
  }
45
64
  }
46
- return expected;
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;
47
71
  }
48
72
 
49
73
  export function rejectUnscopedBoundedWriterDispatch(input: unknown): { block: true; reason: string } | undefined {
50
74
  if (!input || typeof input !== "object" || Array.isArray(input)) return undefined;
51
75
  const record = input as Record<string, unknown>;
52
76
  if (typeof record.agent !== "string" || !WRITER_NAMES.includes(record.agent)) return undefined;
53
- return allowedEditSurfaces(record.task, record.context) ? undefined : { block: true, reason: WRITER_EDIT_SURFACE_REJECTION };
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);
54
101
  }
55
102
 
56
103
  export function isGenericBoundedWriter(name: string): boolean {
@@ -67,11 +114,37 @@ export function isDevelopmentSurface(path: string): boolean {
67
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);
68
115
  }
69
116
 
70
- export function safeBootstrapDirectory(path: string): string | undefined {
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 {
71
144
  try {
72
145
  if (!isAbsolute(path)) return undefined;
73
146
  const root = realpathSync(path);
74
- if (!statSync(root).isDirectory() || root === parse(root).root || root === realpathSync(homedir())) return undefined;
147
+ if (!statSync(root).isDirectory() || root === parse(root).root || isHomeOrBroadUserDirectory(realpathSync.native(root), options)) return undefined;
75
148
  if (/(^|[\\/])(?:\.ssh|\.credentials|\.aws|\.gnupg|\.config|secrets|credentials|tokens|keychains)(?:[\\/]|$)|(?:^|[\\/])\.env(?:$|[./_-])/i.test(root)) return undefined;
76
149
  return root;
77
150
  } catch { return undefined; }
@@ -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,62 @@
1
+ import { isAbsolute } from "node:path";
2
+
3
+ // #1690: the gentle-shell launcher injects the gentle-pi package into the
4
+ // parent pi with `-e` instead of a settings declaration, so delegated children
5
+ // would otherwise start without it. The launcher records the exact extension
6
+ // set it injected in this variable; the subagent runner forwards that set to
7
+ // every child. Kept free of launcher imports so the runner can load it cheaply.
8
+ export const CHILD_PACKAGE_INJECTION_ENV = "GENTLE_SHELL_CHILD_PACKAGE_INJECTION";
9
+
10
+ const CHILD_PACKAGE_INJECTION_VERSION = 1;
11
+
12
+ export interface ChildPackageInjection {
13
+ // True for a launcher takeover: the parent ran with --no-extensions, so the
14
+ // child must too, or settings discovery would load a second gentle-pi.
15
+ noExtensions: boolean;
16
+ // Absolute extension paths, in the order the launcher passed them to `-e`.
17
+ extensionPaths: string[];
18
+ }
19
+
20
+ export function encodeChildPackageInjection(value: ChildPackageInjection): string {
21
+ return JSON.stringify({
22
+ version: CHILD_PACKAGE_INJECTION_VERSION,
23
+ noExtensions: value.noExtensions,
24
+ extensionPaths: value.extensionPaths,
25
+ });
26
+ }
27
+
28
+ // Returns undefined for an absent or invalid value and never throws: a bad
29
+ // signal must degrade to "no forwarding", not break a child launch. The path
30
+ // flavor is injectable so tests can check Windows paths on any platform.
31
+ export function parseChildPackageInjection(
32
+ env: Record<string, string | undefined>,
33
+ pathFlavor: { isAbsolute(path: string): boolean } = { isAbsolute },
34
+ ): ChildPackageInjection | undefined {
35
+ const raw = env[CHILD_PACKAGE_INJECTION_ENV];
36
+ if (raw === undefined || raw.length === 0) return undefined;
37
+ let parsed: unknown;
38
+ try {
39
+ parsed = JSON.parse(raw);
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
44
+ const record = parsed as Record<string, unknown>;
45
+ if (record.version !== CHILD_PACKAGE_INJECTION_VERSION) return undefined;
46
+ if (typeof record.noExtensions !== "boolean") return undefined;
47
+ const paths = record.extensionPaths;
48
+ if (!Array.isArray(paths) || paths.length === 0) return undefined;
49
+ const extensionPaths: string[] = [];
50
+ for (const path of paths) {
51
+ if (typeof path !== "string" || path.length === 0 || !pathFlavor.isAbsolute(path)) return undefined;
52
+ extensionPaths.push(path);
53
+ }
54
+ return { noExtensions: record.noExtensions, extensionPaths };
55
+ }
56
+
57
+ // Plain argv elements for a child pi: spawned without a shell, so no quoting.
58
+ export function childPackageExtensionArgs(injection: ChildPackageInjection): string[] {
59
+ const args = injection.noExtensions ? ["--no-extensions"] : [];
60
+ for (const path of injection.extensionPaths) args.push("--extension", path);
61
+ return args;
62
+ }
@@ -9,8 +9,8 @@ import {
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
  import { getCapabilities, imageFallback, type Component } from "@earendil-works/pi-tui";
11
11
  import {
12
- CARD_TONE, cardAwaitingResult, cardBodyRows, cardBottom, cardLine, cardRunningLine, cardTop, floatRows, markCardResult,
13
- type CardRowContext, type CardTheme, type CardTone,
12
+ CARD_TONE, cardAwaitingResult, cardBodyRows, cardBottom, cardLine, cardRunningLine, cardStyle, cardTop, floatRows, markCardResult,
13
+ type CardRowContext, type CardStyle, type CardTheme, type CardTone,
14
14
  } from "./shell-card.ts";
15
15
  import { sanitizeTerminalText, stripAnsi } from "./terminal-theme.ts";
16
16
 
@@ -87,6 +87,8 @@ class CodemodeCard implements Component {
87
87
  private readonly afterBody: boolean;
88
88
  // The call card's render context: until a result exists, the call closes the frame.
89
89
  private readonly row?: CardRowContext;
90
+ // Content and expansion belong to this component; only layout and row state can change.
91
+ private cachedFrame?: { width: number; style: CardStyle; running: boolean; lines: string[] };
90
92
 
91
93
  constructor(theme: CardTheme, tone: CardTone, body: (width: number) => string[], top: boolean, hint?: string, afterBody = false, row?: CardRowContext) {
92
94
  this.theme = theme;
@@ -102,15 +104,23 @@ class CodemodeCard implements Component {
102
104
  const target = Math.max(0, Math.floor(width));
103
105
  if (target === 0) return [];
104
106
  const running = this.top && this.row !== undefined && cardAwaitingResult(this.row);
105
- return floatRows(this.tone, this.theme, target, (inner) => ({
107
+ const style = cardStyle();
108
+ if (this.cachedFrame?.width === target && this.cachedFrame.style === style && this.cachedFrame.running === running) {
109
+ return this.cachedFrame.lines;
110
+ }
111
+ const lines = floatRows(this.tone, this.theme, target, (inner) => ({
106
112
  head: this.top ? [cardTop({ title: "Code", glyph: "λ", body: [], tone: this.tone }, this.theme, inner, this.hint)] : [],
107
113
  body: running ? [...this.body(inner), cardRunningLine(this.tone, this.theme, inner)] : this.body(inner),
108
114
  bottom: this.top && !running ? undefined : cardBottom(this.tone, this.theme, inner),
109
115
  afterHeading: !this.top && !this.afterBody,
110
116
  }));
117
+ this.cachedFrame = { width: target, style, running, lines };
118
+ return lines;
111
119
  }
112
120
 
113
- invalidate(): void {}
121
+ invalidate(): void {
122
+ this.cachedFrame = undefined;
123
+ }
114
124
  }
115
125
 
116
126
  /** Replace presentation only: execute, schema and every loadout/exposure field retain their references. */
@@ -26,7 +26,7 @@ export const COMMAND_PALETTE_CATALOG: readonly CommandPaletteCatalogGroup[] = [
26
26
  { command: "gentle:review-mode", label: "Review mode (receipt-driven development)" },
27
27
  { command: "gentle:background-subagents", label: "Background subagents" },
28
28
  { command: "gentle:double-esc-cancel", label: "Require double Esc to cancel" },
29
- { command: "gentle:customize", label: "Visual customization" },
29
+ { command: "gentle:customize", label: "Customization · appearance + audio notifications" },
30
30
  { command: "gentle:animations", label: "Animation mode" },
31
31
  { command: "gentle:vim", label: "Vim opt-in · Pi slash commands" },
32
32
  { command: "gentle:telemetry", label: "Telemetry" },
@@ -41,8 +41,9 @@ export const COMMAND_PALETTE_CATALOG: readonly CommandPaletteCatalogGroup[] = [
41
41
  title: "Session",
42
42
  items: [
43
43
  { command: "gentle:yolo", label: "🚀 YOLO 🔥 session permission" },
44
- { command: "gentle:changes", label: "Browse captured changes" },
44
+ { command: "gentle:changes", label: "Browse changes" },
45
45
  { command: "gentle:agents", label: "Subagents" },
46
+ { command: "gentle:jobs", label: "Background jobs" },
46
47
  { command: "gentle:usage", label: "Subscription usage" },
47
48
  { command: "gentle:review-session-permission", label: "Review session permission" },
48
49
  ],
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Fixed product threshold for the high-context warning. The latch lives outside
3
+ * the notification and visual stores so both warnings share one episode.
4
+ */
5
+ export const CONTEXT_HIGH_PERCENT = 90;
6
+
7
+ /**
8
+ * One warning per high episode. `observe` returns true exactly once while usage
9
+ * stays at or above the threshold; a finite reading below it rearms the latch.
10
+ * An unknown reading (`null`, which Pi reports right after a successful
11
+ * compaction until the next assistant response) preserves the latch so the same
12
+ * episode cannot warn twice. A successful compaction or a new session calls
13
+ * `rearm` explicitly; a canceled or failed compaction must not, because the
14
+ * context was never reduced.
15
+ */
16
+ export class ContextHighLatch {
17
+ private high = false;
18
+ observe(percent: number | null | undefined): boolean {
19
+ if (typeof percent !== "number" || !Number.isFinite(percent)) return false;
20
+ if (percent < CONTEXT_HIGH_PERCENT) { this.high = false; return false; }
21
+ if (this.high) return false;
22
+ this.high = true;
23
+ return true;
24
+ }
25
+ rearm(): void { this.high = false; }
26
+ }