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,9 +1,9 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { Text, type Component, type TUI } from "@earendil-works/pi-tui";
2
+ import { ScrollView, Text, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
3
3
  import { appendSystemPromptOnce } from "../lib/append-system-prompt.ts";
4
4
  import { NativePointerRegion } from "../lib/native-pointer-region.ts";
5
5
  import { panelHeaderRow } from "../lib/shell-card.ts";
6
- import { sidebarPart } from "../lib/shell-sidebar.ts";
6
+ import { sidebarPart, sidebarState } from "../lib/shell-sidebar.ts";
7
7
  import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
8
8
  import {
9
9
  applyTodo,
@@ -16,6 +16,7 @@ import {
16
16
  TODO_TOOL_NAME,
17
17
  todoCardTone,
18
18
  todoPromptBlock,
19
+ todoStateMessage,
19
20
  todoSummary,
20
21
  type TodoParams,
21
22
  type TodoState,
@@ -24,12 +25,18 @@ import {
24
25
  // Gentle Todo: the task list the model keeps while it works, drawn as a
25
26
  // Gentle Shell card above the editor. Three things keep it current that a
26
27
  // static tool description cannot: `write` replaces the whole list in one
27
- // call, every turn's system prompt carries the open tasks and the rules, and
28
- // a list that goes untouched while tasks stay open is marked stale for both
29
- // the human and the model.
28
+ // call, each run's system prompt carries a byte-stable rules block while the
29
+ // open tasks reach the model as a request-local trailing message (so list
30
+ // changes never invalidate the cached prompt prefix, gentle-shell#1662), and
31
+ // a list that goes untouched while tasks stay open is marked stale for the
32
+ // human. Long runs also receive one request-local reminder after several
33
+ // tool-use turns, without waking the agent or changing tasks.
30
34
 
31
35
  const WIDGET_KEY = "gentle-todo";
36
+ const STATUS_ENUM = ["pending", "in_progress", "blocked", "done", "dropped"];
32
37
  const COLLAPSE_KEY_DEFAULT = "ctrl+shift+t";
38
+ const REMIND_AFTER_TOOL_TURNS = 4;
39
+ const TODO_REMINDER = "The todo plan has not been reviewed during several tool-use turns. Check whether it still matches the work performed and update tasks where needed. Do not mark work done without verification.";
33
40
  const TOOL_PARAMETERS = {
34
41
  type: "object",
35
42
  additionalProperties: false,
@@ -46,15 +53,15 @@ const TOOL_PARAMETERS = {
46
53
  properties: {
47
54
  id: { type: "integer", description: "Existing task id to keep." },
48
55
  title: { type: "string", description: "Short imperative title, e.g. 'Write the parser'." },
49
- status: { type: "string", enum: ["pending", "in_progress", "done"], description: "Defaults to pending." },
50
- note: { type: "string", description: "What is happening right now, shown while in_progress, e.g. 'writing tests'." },
56
+ status: { type: "string", enum: STATUS_ENUM, description: "Defaults to pending. blocked: open but waiting on something outside the list; dropped: will not be done, on purpose." },
57
+ note: { type: "string", description: "What is happening right now, shown while in_progress, e.g. 'writing tests'; required for blocked, naming what it waits for, e.g. 'waiting for an admin'." },
51
58
  },
52
59
  },
53
60
  },
54
61
  id: { type: "integer", description: "Task id for update." },
55
62
  title: { type: "string", description: "Title for add, or a new title for update." },
56
- status: { type: "string", enum: ["pending", "in_progress", "done"], description: "Status for add or update." },
57
- note: { type: "string", description: "Note for add or update." },
63
+ status: { type: "string", enum: STATUS_ENUM, description: "Status for add or update." },
64
+ note: { type: "string", description: "Note for add or update; required for blocked." },
58
65
  },
59
66
  } as const;
60
67
 
@@ -73,12 +80,18 @@ export function todoCollapseKey(env: NodeJS.ProcessEnv = process.env): string |
73
80
  interface TodoSession {
74
81
  state: TodoState;
75
82
  turn: number;
83
+ /** Separate from the existing run-based card freshness. */
84
+ toolTurnsSinceWrite: number;
85
+ reminded: boolean;
86
+ wroteThisTurn: boolean;
76
87
  collapsed: boolean;
77
88
  /** A finished list stays on screen for the turn it finished in, then clears. */
78
89
  clearOnNextTurn: boolean;
79
90
  ui: ExtensionContext["ui"] | undefined;
80
91
  host: { requestRender(): void } | undefined;
81
92
  tui: TUI | undefined;
93
+ /** Resolve the visible host when the key is pressed, not when it renders. */
94
+ scrollTodo?: (lines: number) => void;
82
95
  }
83
96
 
84
97
  function sessionKey(ctx: ExtensionContext): string {
@@ -94,7 +107,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
94
107
  const key = sessionKey(ctx);
95
108
  let current = sessions.get(key);
96
109
  if (!current) {
97
- current = { state: emptyTodo(), turn: 0, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
110
+ current = { state: emptyTodo(), turn: 0, toolTurnsSinceWrite: 0, reminded: false, wroteThisTurn: false, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
98
111
  sessions.set(key, current);
99
112
  }
100
113
  return current;
@@ -106,8 +119,20 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
106
119
  current.host?.requestRender();
107
120
  };
108
121
 
109
- const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1], scrollable: boolean, spacer: boolean): Component & { dispose(): void } => {
122
+ const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1], spacer: boolean): Component & { dispose(): void; digest(): string; scrollBy(lines: number): void } => {
110
123
  let hovered = false;
124
+ let body: string[] = [];
125
+ let collapsed = current.collapsed;
126
+ // Widgets are measured as leaves, so native layout cannot assign this
127
+ // body a viewport. Use native scroll state and explicitly slice its lines
128
+ // while keeping the collapse header and card footer outside the viewport.
129
+ const scroll = new ScrollView({ render: () => body, invalidate() {} }, { follow: "none", overscroll: "contain" });
130
+ const scrollBy = (lines: number) => {
131
+ if (current.collapsed) return;
132
+ scroll.scrollBy(lines);
133
+ if (current.tui) invalidateSidebar(current.tui);
134
+ current.host?.requestRender();
135
+ };
111
136
  // The row the rendered card draws its header on: 0 for the outlined
112
137
  // frame, 1 below the float panel's top padding row.
113
138
  let headerRow = 0;
@@ -120,9 +145,25 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
120
145
  staleTurns: stale,
121
146
  collapseKey,
122
147
  hovered,
123
- ...(scrollable ? { scrollable: true } : {}),
148
+ scrollable: true,
124
149
  });
125
- return spacer && lines.length > 0 ? [...lines, ""] : lines;
150
+ if (collapsed !== current.collapsed) {
151
+ collapsed = current.collapsed;
152
+ scroll.scrollToStart();
153
+ }
154
+ // At most one third of the screen, capped at 16 rows. Read height
155
+ // on every render so a mobile resize immediately releases chat space.
156
+ const terminalRows = current.tui?.terminal?.rows ?? 48;
157
+ const height = Math.min(16, Math.floor(terminalRows / 3));
158
+ const bodyStart = headerRow === 1 ? 3 : 1;
159
+ body = lines.slice(bodyStart, -1);
160
+ const overflow = !current.collapsed && lines.length + Number(spacer) > height;
161
+ const room = Math.max(1, height - bodyStart - 1 - Number(spacer) - Number(overflow));
162
+ scroll.updateLayout(body.length, current.collapsed ? body.length : room, () => current.host?.requestRender());
163
+ const visible = scroll.render(width).slice(scroll.scrollTop, scroll.scrollTop + scroll.viewportHeight);
164
+ const output = [...lines.slice(0, bodyStart), ...visible, ...lines.slice(-1)];
165
+ if (overflow) output.push(theme.fg("muted", truncateToWidth(`↑↓ ${scroll.scrollTop + 1}–${scroll.scrollTop + visible.length}/${body.length} · ctrl+shift+↑/↓`, width)));
166
+ return spacer && output.length > 0 ? [...output, ""] : output;
126
167
  },
127
168
  invalidate() {
128
169
  hovered = false;
@@ -148,8 +189,17 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
148
189
  toggle(current);
149
190
  return { handled: true, render: true };
150
191
  },
192
+ onWheel(event) {
193
+ if (current.collapsed || body.length <= scroll.viewportHeight) return undefined;
194
+ scrollBy(event.wheelDelta ?? 0);
195
+ return { handled: true, render: true };
196
+ },
151
197
  });
152
198
  return {
199
+ scrollBy,
200
+ // Sidebar caches must observe height-only resizes and local viewport
201
+ // changes, even when terminal width and task content stay the same.
202
+ digest: () => JSON.stringify([current.tui?.terminal?.rows, current.collapsed, current.turn, hovered, scroll.scrollTop]),
153
203
  render: (width) => region.render(width),
154
204
  handleMouse: (event) => region.handleMouse(event),
155
205
  invalidate: () => region.invalidate(),
@@ -161,6 +211,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
161
211
  if (current.tui) invalidateSidebar(current.tui);
162
212
  if (!current.ui) return;
163
213
  if (current.state.tasks.length === 0) {
214
+ current.scrollTodo = undefined;
164
215
  current.ui.setWidget(WIDGET_KEY, undefined);
165
216
  return;
166
217
  }
@@ -168,7 +219,15 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
168
219
  current.ui.setWidget(WIDGET_KEY, (tui, theme) => {
169
220
  snapshot.host = tui;
170
221
  snapshot.tui = tui;
171
- return sidebarPart(tui, "todo", todoCard(snapshot, theme, false, true), todoCard(snapshot, theme, true, false));
222
+ const bottom = todoCard(snapshot, theme, true);
223
+ const rail = todoCard(snapshot, theme, false);
224
+ snapshot.scrollTodo = (lines) => {
225
+ // Cached rail lines do not re-render when a narrow→wide resize
226
+ // restores the sidebar. Select ownership from the live layout.
227
+ const state = tui.terminal ? sidebarState(tui) : undefined;
228
+ (state?.active && state.ownsHost?.() ? rail : bottom).scrollBy(lines);
229
+ };
230
+ return sidebarPart(tui, "todo", bottom, rail);
172
231
  });
173
232
  };
174
233
 
@@ -183,6 +242,8 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
183
242
  "Mark a task in_progress before starting it and done right after finishing it; keep exactly one task in_progress.",
184
243
  "Prefer write with the complete list whenever the plan changes; keep ids of tasks that already exist.",
185
244
  "Never mark a task done while tests fail or the work is partial; add a task for the blocker instead.",
245
+ "Mark a task blocked with a note naming what it waits for only when that is outside the list (a person, another team, an authorization); a prerequisite in the list is ordering, so keep the task pending. Return it to pending once the condition holds.",
246
+ "When the plan changes, mark tasks it made obsolete dropped, never done; dropped tasks no longer count as open.",
186
247
  ],
187
248
  parameters: TOOL_PARAMETERS,
188
249
  executionMode: "sequential",
@@ -200,6 +261,11 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
200
261
  if (!result.error) {
201
262
  current.state = result.state;
202
263
  current.clearOnNextTurn = false;
264
+ if (params.action !== "list") {
265
+ current.toolTurnsSinceWrite = 0;
266
+ current.reminded = false;
267
+ current.wroteThisTurn = true;
268
+ }
203
269
  if (current.tui) invalidateSidebar(current.tui);
204
270
  }
205
271
  return {
@@ -218,6 +284,15 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
218
284
  });
219
285
  }
220
286
 
287
+ // Pi binds ctrl+shift+up/down to prompt navigation (tui.altScreen.*Prompt) and
288
+ // alt+up/down to dequeue and model reordering; alt+shift+up/down are free.
289
+ for (const [key, lines] of [["alt+shift+up", -3], ["alt+shift+down", 3]] as const) {
290
+ pi.registerShortcut(key, {
291
+ description: `Scroll the Todo list ${lines < 0 ? "up" : "down"}`,
292
+ handler: async (ctx) => session(ctx).scrollTodo?.(lines),
293
+ });
294
+ }
295
+
221
296
  pi.on("session_start", (_event, ctx) => {
222
297
  const current = session(ctx);
223
298
  // A list that was already finished when the session was left is history,
@@ -225,6 +300,9 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
225
300
  const replayed = replayTodo(ctx.sessionManager.getBranch());
226
301
  current.state = replayed.tasks.length > 0 && todoSummary(replayed).open === 0 ? { ...replayed, tasks: [] } : replayed;
227
302
  current.turn = ctx.sessionManager.getBranch().filter((entry) => (entry as { type?: string }).type === "message" && (entry as { message?: { role?: string } }).message?.role === "user").length;
303
+ current.toolTurnsSinceWrite = 0;
304
+ current.reminded = false;
305
+ current.wroteThisTurn = false;
228
306
  current.ui = ctx.hasUI ? ctx.ui : undefined;
229
307
  show(current);
230
308
  });
@@ -242,14 +320,41 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
242
320
  current.clearOnNextTurn = false;
243
321
  show(current);
244
322
  }
245
- const block = todoPromptBlock(current.state, staleTurns(current.state, current.turn));
246
- if (!block) return undefined;
247
323
  // gentle-shell#1485: pi-claude-bridge drops a handler-returned
248
- // systemPrompt, so the open-tasks block goes through appendSystemPrompt.
249
- appendSystemPromptOnce(event.systemPromptOptions, block);
324
+ // systemPrompt, so the rules block goes through appendSystemPrompt. It is
325
+ // the same bytes on every turn, list or no list (gentle-shell#1662).
326
+ appendSystemPromptOnce(event.systemPromptOptions, todoPromptBlock());
250
327
  return undefined;
251
328
  });
252
329
 
330
+ pi.on("turn_end", (event, ctx) => {
331
+ const current = session(ctx);
332
+ // A turn containing a valid todo write is already reconciled. Failed
333
+ // writes and list reads deliberately do not refresh this counter.
334
+ if (!current.wroteThisTurn && event.message.role === "assistant" && event.message.content.some((part) => part.type === "toolCall")) {
335
+ current.toolTurnsSinceWrite += 1;
336
+ }
337
+ current.wroteThisTurn = false;
338
+ });
339
+
340
+ pi.on("context", (event, ctx) => {
341
+ const current = session(ctx);
342
+ // Context transforms are request-local: no transcript entry, UI
343
+ // notification, continuation or provider-specific wake is needed. The
344
+ // current list trails the transcript (gentle-shell#1662), then the
345
+ // one-shot reminder when it is due.
346
+ const trailing: Array<{ role: "custom"; customType: string; content: string; display: false; timestamp: number }> = [];
347
+ const state = todoStateMessage(current.state);
348
+ if (state !== undefined) trailing.push({ role: "custom", customType: "gentle-todo-state", content: state, display: false, timestamp: Date.now() });
349
+ const actionable = current.state.tasks.some((task) => task.status === "pending" || task.status === "in_progress");
350
+ if (!current.reminded && current.toolTurnsSinceWrite >= REMIND_AFTER_TOOL_TURNS && actionable) {
351
+ current.reminded = true;
352
+ trailing.push({ role: "custom", customType: "gentle-todo-reminder", content: TODO_REMINDER, display: false, timestamp: Date.now() });
353
+ }
354
+ if (trailing.length === 0) return undefined;
355
+ return { messages: [...event.messages, ...trailing] };
356
+ });
357
+
253
358
  pi.on("tool_execution_end", (event, ctx) => {
254
359
  if (event.toolName !== TODO_TOOL_NAME) return;
255
360
  show(session(ctx));
@@ -37,7 +37,7 @@ import {
37
37
  type TuiMouseEvent,
38
38
  truncateToWidth,
39
39
  } from "@earendil-works/pi-tui";
40
- import { gentlePiConfigHome } from "../../lib/agent-home.ts";
40
+ import { bundledGentlePiAgentHome, gentlePiConfigHome } from "../../lib/agent-home.ts";
41
41
  import {
42
42
  historyCaptureEnabled,
43
43
  historyCaptureEnvOverride,
@@ -1316,14 +1316,19 @@ export default function promptHistoryExtension(
1316
1316
  ): void {
1317
1317
  const env = deps.env ?? process.env;
1318
1318
  const configHome = deps.gentlePiConfigHome ?? gentlePiConfigHome(env);
1319
- // Per-prompt gate: re-read so a Customize toggle applies live.
1320
- const capturing = () => captureEnabled(env, configHome);
1321
- const root = deps.root ?? PI_HISTORY_ROOT;
1319
+ // Per-prompt gate: re-read so a Customize toggle applies live. A delegated
1320
+ // child never captures: its prompt is a delegation brief, not user history,
1321
+ // and the parent owns the store's init and GC (gentle-shell#1690).
1322
+ const capturing = () => env.GENTLE_PI_AGENTS_CHILD !== "1" && captureEnabled(env, configHome);
1323
+ // S9: a bundled Gentle Shell keeps the whole store in the agent home Pi runs
1324
+ // with; everywhere else it stays under ~/.pi/agent, where existing ones live.
1325
+ const bundledAgentDir = bundledGentlePiAgentHome(env);
1326
+ const root = deps.root ?? (bundledAgentDir === undefined ? PI_HISTORY_ROOT : join(bundledAgentDir, "history"));
1322
1327
  const cwd = deps.cwd ?? process.cwd();
1323
1328
  const instanceId = deps.instanceId ?? randomUUID();
1324
1329
  const now = deps.now ?? Date.now;
1325
- const agentDir = deps.agentDir ?? AGENT_DIR;
1326
- const sessionsRoot = deps.sessionsRoot ?? SESSIONS_ROOT;
1330
+ const agentDir = deps.agentDir ?? bundledAgentDir ?? AGENT_DIR;
1331
+ const sessionsRoot = deps.sessionsRoot ?? (bundledAgentDir === undefined ? SESSIONS_ROOT : join(bundledAgentDir, "sessions"));
1327
1332
  let writerState: SessionWriterState | null = null;
1328
1333
 
1329
1334
  /**
@@ -1,5 +1,14 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { shellEnabled } from "../lib/shell-bar.ts";
1
+ import {
2
+ createBashToolDefinition,
3
+ createFindToolDefinition,
4
+ createGrepToolDefinition,
5
+ createLsToolDefinition,
6
+ createReadToolDefinition,
7
+ type ExtensionAPI,
8
+ type ExtensionContext,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import { bundledGentlePiAgentHome } from "../lib/agent-home.ts";
11
+ import { keepNativeWorkingRow, shellEnabled } from "../lib/shell-bar.ts";
3
12
  import {
4
13
  mergeDisabledTools,
5
14
  PI_PRETTY_SUPPRESSED_TOOL_NAMES,
@@ -26,12 +35,37 @@ async function loadPiPrettyExtension(): Promise<PiPrettyExtension> {
26
35
  );
27
36
  }
28
37
 
38
+ // S9: pi-pretty keeps its FFF database under deps.sdk.getAgentDir(), falling
39
+ // back to $HOME/.pi/agent whatever agent home Pi runs with. In a bundled
40
+ // Gentle Shell it gets the agent home; a deps.sdk also replaces the host tool
41
+ // factories it would otherwise import, so the same five come along. Elsewhere,
42
+ // and for a host that already passes its own sdk, Pi's deps stay unchanged.
43
+ function withAgentHome(deps: unknown, env: NodeJS.ProcessEnv): unknown {
44
+ const agentHome = bundledGentlePiAgentHome(env);
45
+ const given = (deps ?? {}) as { sdk?: unknown };
46
+ if (agentHome === undefined || given.sdk !== undefined) return deps;
47
+ return {
48
+ ...given,
49
+ sdk: {
50
+ createReadToolDefinition,
51
+ createBashToolDefinition,
52
+ createLsToolDefinition,
53
+ createFindToolDefinition,
54
+ createGrepToolDefinition,
55
+ getAgentDir: () => agentHome,
56
+ },
57
+ };
58
+ }
59
+
29
60
  export default async function gentlePiPrettyExtension(
30
61
  pi: unknown,
31
62
  deps?: unknown,
32
63
  bundled?: PiPrettyExtension,
33
64
  env: NodeJS.ProcessEnv = process.env,
34
65
  ): Promise<unknown> {
66
+ // gentle-shell#1690: a delegated child has no transcript to prettify, and
67
+ // the upstream fallback would start its own file indexing in every child.
68
+ if (env.GENTLE_PI_AGENTS_CHILD === "1") return undefined;
35
69
  if (quietToolsEnabled()) {
36
70
  process.env.PRETTY_DISABLE_TOOLS = mergeDisabledTools(
37
71
  process.env.PRETTY_DISABLE_TOOLS,
@@ -39,6 +73,7 @@ export default async function gentlePiPrettyExtension(
39
73
  );
40
74
  }
41
75
  const extension = bundled ?? await loadPiPrettyExtension();
76
+ deps = withAgentHome(deps, env);
42
77
  if (!shellEnabled(env)) return extension(pi, deps);
43
78
  const api = pi as ExtensionAPI;
44
79
  // Scope interception to this bundled dependency, never to the shared host UI.
@@ -77,8 +112,9 @@ export default async function gentlePiPrettyExtension(
77
112
  // GentlePromptEditor already carries the live working state in its frame.
78
113
  // Hide Pi's separate loader row to avoid repeating Thinking above the input;
79
114
  // transcript thinking blocks remain untouched as historical reasoning markers.
115
+ // Inside Herdr the row stays: its native Pi detection reads it.
80
116
  api.on("session_start", (_event, ctx) => {
81
- if (ctx.mode === "tui") ctx.ui.setWorkingVisible(false);
117
+ if (ctx.mode === "tui" && !keepNativeWorkingRow(env)) ctx.ui.setWorkingVisible(false);
82
118
  });
83
119
  api.on("session_shutdown", (_event, ctx) => {
84
120
  if (ctx.mode !== "tui") return;
@@ -12,12 +12,14 @@ import { Text, type Component } from "@earendil-works/pi-tui";
12
12
  import { homedir } from "node:os";
13
13
  import { isAbsolute } from "node:path";
14
14
  import { resolveGentleAiDevBinaryOverride, type GentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
15
+ import { CARD_CONTENT, cardContent } from "../lib/card-content-policy.ts";
15
16
  import { GentleAiElapsedTimingLedger } from "../lib/gentle-ai-elapsed-store.ts";
16
17
  import { quietToolsEnabled } from "../lib/quiet-tools-config.ts";
17
18
  import { registerCompactCodemode } from "../lib/codemode-renderer.ts";
19
+ import { offerBuiltinCodemodeOptOut, type BuiltinCodemodeOptOutOptions } from "../lib/builtin-codemode-optout.ts";
18
20
  import { getGentleAiRenderState, renderGentleAiLifecycleCall, renderGentleAiResult, type GentleAiRenderContext } from "../lib/gentle-ai-renderer.ts";
19
21
  import {
20
- CARD_TONE, cardAwaitingResult, cardBottom, cardInnerWidth, cardLine, cardRunningLine, cardTopRows, floatRows, markCardResult,
22
+ CARD_TONE, cardAwaitingResult, cardBottom, cardInnerWidth, cardLine, cardRunningLine, cardStyle, cardTopRows, floatRows, markCardResult,
21
23
  type CardRowContext, type CardTheme,
22
24
  } from "../lib/shell-card.ts";
23
25
  import { sanitizeTerminalText, stripAnsi } from "../lib/terminal-theme.ts";
@@ -55,6 +57,11 @@ const COLLAPSED_COUNT_LABELS: Partial<Record<QuietToolName, string>> = {
55
57
  ls: "entries",
56
58
  };
57
59
 
60
+ // Cards that show only the command while the Card content preference is
61
+ // minimal: their results stay one expand key away and never add rows to the
62
+ // transcript view.
63
+ const COMMAND_ONLY_TOOLS: ReadonlySet<QuietToolName> = new Set(["read", "bash", "grep", "find", "ls", "edit", "write"]);
64
+
58
65
  const COLLAPSED_TAIL_LINE_LIMIT = 10;
59
66
  const PREVIEW_LINE_LIMIT = 3;
60
67
 
@@ -423,7 +430,10 @@ export function formatToolResultOutput(
423
430
  ): string {
424
431
  const text = safeText(extractTextContent(result));
425
432
  if (expanded) {
426
- const detail = expandedResultText(toolName, result, text);
433
+ // Write results only acknowledge the operation; the body lives in the call args.
434
+ const detail = toolName === "write" && !isError && typeof args?.content === "string" && args.content.length > 0
435
+ ? safeText(args.content)
436
+ : expandedResultText(toolName, result, text);
427
437
  return detail ? `\n${detail}` : "";
428
438
  }
429
439
  if (isError) {
@@ -470,7 +480,9 @@ interface ToolRenderContextLike {
470
480
  lastComponent?: unknown;
471
481
  state?: unknown;
472
482
  cwd?: string;
473
- [key: string]: unknown;
483
+ expanded?: boolean;
484
+ toolCallId?: string;
485
+ durationMs?: number;
474
486
  }
475
487
 
476
488
  function formatToolCall(toolName: QuietToolName, args: Record<string, unknown>, theme: ThemeLike): string {
@@ -482,7 +494,9 @@ function formatToolCall(toolName: QuietToolName, args: Record<string, unknown>,
482
494
  case "bash": {
483
495
  const command = safeText(asString(args.command, "..."));
484
496
  const timeout = typeof args.timeout === "number" ? theme.fg("muted", ` (timeout ${args.timeout}s)`) : "";
485
- return `${theme.fg("toolTitle", theme.bold(`bash $ ${command}`))}${timeout}`;
497
+ // Minimal cards title bash with the bare command; the card glyph supplies the `$` prompt.
498
+ const title = cardContent() === CARD_CONTENT.MINIMAL ? command : `bash $ ${command}`;
499
+ return `${theme.fg("toolTitle", theme.bold(title))}${timeout}`;
486
500
  }
487
501
  case "grep": {
488
502
  let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${safeText(asString(args.pattern))}/`)} in ${safeText(shortenPath(args.path) || ".")}`;
@@ -568,7 +582,23 @@ function toolTone(pending: boolean, failed: boolean): ToolTone {
568
582
  return pending ? CARD_TONE.WARNING : CARD_TONE.SUCCESS;
569
583
  }
570
584
 
585
+ /** One settled frame per component: bounded memory, including all card chrome. */
586
+ class ToolCardRows {
587
+ private frame?: { width: number; style: ReturnType<typeof cardStyle>; running: boolean; lines: string[] };
588
+
589
+ render(width: number, running: boolean, build: () => string[]): string[] {
590
+ const style = cardStyle();
591
+ if (this.frame?.width === width && this.frame.style === style && this.frame.running === running) return this.frame.lines;
592
+ const lines = build();
593
+ this.frame = { width, style, running, lines };
594
+ return lines;
595
+ }
596
+
597
+ invalidate(): void { this.frame = undefined; }
598
+ }
599
+
571
600
  class ToolCardTop implements Component {
601
+ private readonly rows = new ToolCardRows();
572
602
  private readonly header: () => string;
573
603
  private readonly glyph: string;
574
604
  private readonly tone: ToolTone;
@@ -590,41 +620,44 @@ class ToolCardTop implements Component {
590
620
  const target = Math.max(0, Math.floor(width));
591
621
  if (target === 0) return [];
592
622
  const running = this.row !== undefined && cardAwaitingResult(this.row);
593
- return floatRows(this.tone, this.theme, target, (inner) => ({
623
+ return this.rows.render(target, running, () => floatRows(this.tone, this.theme, target, (inner) => ({
594
624
  head: cardTopRows({ title: this.header(), glyph: this.glyph, body: [], tone: this.tone }, this.theme, inner, this.hint),
595
625
  body: running ? [cardRunningLine(this.tone, this.theme, inner)] : undefined,
596
626
  bottom: running ? cardBottom(this.tone, this.theme, inner) : undefined,
597
- }));
627
+ })));
598
628
  }
599
629
 
600
- invalidate(): void {}
630
+ invalidate(): void { this.rows.invalidate(); }
601
631
  }
602
632
 
603
633
  class ToolCardBody implements Component {
634
+ private readonly rows = new ToolCardRows();
604
635
  private readonly inner: () => Component;
605
636
  private readonly tone: ToolTone;
606
637
  private readonly theme: CardTheme;
607
638
 
608
- constructor(inner: () => Component, tone: ToolTone, theme: CardTheme) {
639
+ private readonly cacheable: boolean;
640
+
641
+ constructor(inner: () => Component, tone: ToolTone, theme: CardTheme, cacheable = true) {
609
642
  this.inner = inner;
610
643
  this.tone = tone;
611
644
  this.theme = theme;
645
+ this.cacheable = cacheable;
612
646
  }
613
647
 
614
648
  /** Renders the inner component between the card sides and closes the frame, even when the result has no rows. */
615
649
  render(width: number): string[] {
616
650
  const target = Math.max(0, Math.floor(width));
617
651
  if (target === 0) return [];
618
- return floatRows(this.tone, this.theme, target, (inner) => ({
652
+ const build = () => floatRows(this.tone, this.theme, target, (inner) => ({
619
653
  body: this.inner().render(cardInnerWidth(inner)).map((line) => cardLine(line.trimEnd(), this.tone, this.theme, inner)),
620
654
  bottom: cardBottom(this.tone, this.theme, inner),
621
655
  afterHeading: true,
622
656
  }));
657
+ return this.cacheable ? this.rows.render(target, false, build) : build();
623
658
  }
624
659
 
625
- invalidate(): void {
626
- // Content is rebuilt with the current theme at render time.
627
- }
660
+ invalidate(): void { this.rows.invalidate(); }
628
661
  }
629
662
 
630
663
  function shouldRenderPreviewTail(
@@ -649,11 +682,15 @@ function hasImageContent(result: AgentToolResult<unknown>): boolean {
649
682
  return result.content.some((content) => content.type === "image");
650
683
  }
651
684
 
652
- function sanitizedRenderContext(context: ToolRenderContextLike | undefined): ToolRenderContextLike {
685
+ function isRenderArgs(value: unknown): value is Record<string, unknown> {
686
+ return typeof value === "object" && value !== null && !Array.isArray(value);
687
+ }
688
+
689
+ function sanitizedRenderContext(context: (Omit<ToolRenderContextLike, "args"> & { args?: unknown }) | undefined): ToolRenderContextLike {
653
690
  if (!context) return { args: {} };
654
691
  return {
655
692
  ...context,
656
- args: sanitizedArgs(context.args),
693
+ args: sanitizedArgs(isRenderArgs(context.args) ? context.args : undefined),
657
694
  cwd: typeof context.cwd === "string" ? safeText(context.cwd) : context.cwd,
658
695
  };
659
696
  }
@@ -685,7 +722,7 @@ function gentleAiRenderTransition(
685
722
  return { directResult: false };
686
723
  }
687
724
 
688
- /** Rendering-only factory; Bash renderers are not attached to production native Bash. */
725
+ /** Rendering-only factory; the default export attaches the Bash renderers to native Bash through pi.registerToolRenderer, so execution and schema stay pi's. */
689
726
  export function createQuietToolRenderer(
690
727
  toolName: QuietToolName,
691
728
  resolveOverride: GentleAiDevBinaryOverrideResolver = () => undefined,
@@ -702,7 +739,7 @@ export function createQuietToolRenderer(
702
739
  renderShell: "self",
703
740
  renderCall(args, theme, context) {
704
741
  const callArgs = args as Record<string, unknown>;
705
- const renderContext = sanitizedRenderContext(context as ToolRenderContextLike | undefined);
742
+ const renderContext = sanitizedRenderContext(context);
706
743
  const operationPath = toolName === "bash"
707
744
  ? gentleAiRenderTransition(callArgs, renderContext, commandArguments()).operationPath
708
745
  : undefined;
@@ -718,12 +755,15 @@ export function createQuietToolRenderer(
718
755
  },
719
756
  /** Builds the card component for this render pass; collapsed cards delegate to the wrapped-line cache keyed by the tool result object. */
720
757
  renderResult(result, options, theme, context) {
721
- const renderContext = context as ToolRenderContextLike | undefined;
758
+ const renderContext = context ? sanitizedRenderContext(context) : undefined;
722
759
  markCardResult(renderContext?.state);
723
760
  const cacheKey = typeof result === "object" && result !== null ? result : undefined;
724
761
  const safeResult = sanitizedResult(result);
725
762
  const text = safeText(extractTextContent(safeResult));
726
- const isError = renderContext?.isError ?? options.isError ?? false;
763
+ // Current Pi carries errors on the context. Older renderer callers
764
+ // supplied them on options; keep that structural fallback for those hosts.
765
+ const legacyError = "isError" in options && options.isError === true;
766
+ const isError = renderContext?.isError ?? legacyError;
727
767
  const directResult = toolName === "bash" && gentleAiRenderTransition(
728
768
  renderContext?.args,
729
769
  renderContext,
@@ -734,7 +774,14 @@ export function createQuietToolRenderer(
734
774
  return renderGentleAiResult(safeResult, { expanded: options.expanded, isPartial: options.isPartial, isError }, theme, renderContext ? withElapsedTiming(renderContext as GentleAiRenderContext) : undefined);
735
775
  }
736
776
  const resultTone = toolTone(options.isPartial === true, isError);
737
- const carded = (component: () => Component): Component => new ToolCardBody(component, resultTone, theme);
777
+ const carded = (component: () => Component, cacheable = options.isPartial !== true): Component => new ToolCardBody(component, resultTone, theme, cacheable);
778
+ // Under the minimal Card content preference, every quiet tool draws
779
+ // the command alone while collapsed; the expand key still reveals
780
+ // the full result. Failures keep their bounded error tail so a red
781
+ // card always says why. The default preference keeps the previews.
782
+ if (COMMAND_ONLY_TOOLS.has(toolName) && !options.expanded && !isError && cardContent() === CARD_CONTENT.MINIMAL) {
783
+ return carded(() => new Text("", 0, 0));
784
+ }
738
785
  if (options.isPartial) {
739
786
  if (options.expanded) return carded(() => new Text(`${theme.fg("warning", partialLabel(toolName, text))}\n${theme.fg("muted", text)}`, 0, 0));
740
787
  const visible = lastOutputLines(text, PREVIEW_LINE_LIMIT);
@@ -749,7 +796,7 @@ export function createQuietToolRenderer(
749
796
  options,
750
797
  theme,
751
798
  sanitizedRenderContext(renderContext) as any,
752
- ));
799
+ ), false);
753
800
  }
754
801
  let output = formatToolResultOutput(toolName, safeResult, {
755
802
  expanded: options.expanded,
@@ -789,11 +836,24 @@ function registerQuietTool(pi: ExtensionAPI, toolName: RegisteredToolName, resol
789
836
  });
790
837
  }
791
838
 
792
- export default function quietTools(pi: ExtensionAPI, resolveOverride: GentleAiDevBinaryOverrideResolver = () => resolveGentleAiDevBinaryOverride()): ReturnType<ExtensionFactory> {
839
+ export default function quietTools(
840
+ pi: ExtensionAPI,
841
+ resolveOverride: GentleAiDevBinaryOverrideResolver = () => resolveGentleAiDevBinaryOverride(),
842
+ codemodeOptOut: Omit<BuiltinCodemodeOptOutOptions, "effectiveExtensions"> = {},
843
+ ): ReturnType<ExtensionFactory> {
793
844
  if (!quietToolsEnabled()) return;
794
845
  let elapsedTiming: GentleAiElapsedTimingLedger | undefined;
846
+ let codemodeOptOutOffered = false;
795
847
  pi.on("session_start", (_event, ctx) => {
796
848
  elapsedTiming = new GentleAiElapsedTimingLedger(ctx.sessionManager, pi);
849
+ // The compact codemode below displaces Pi's builtin, which makes Pi warn
850
+ // at startup. Offer the settings opt-out once per process, detached so
851
+ // the dialog never holds up startup; the offer itself never throws.
852
+ if (codemodeOptOutOffered) return;
853
+ codemodeOptOutOffered = true;
854
+ let effectiveExtensions: unknown;
855
+ try { effectiveExtensions = pi.getSettings().extensions; } catch { /* Fall back to the settings file alone. */ }
856
+ void offerBuiltinCodemodeOptOut(ctx, { ...codemodeOptOut, effectiveExtensions });
797
857
  });
798
858
  // Only bash calls that render as gentle-ai cards carry a durable duration;
799
859
  // every other quiet tool keeps its plain renderer and writes no entries.
@@ -812,5 +872,15 @@ export default function quietTools(pi: ExtensionAPI, resolveOverride: GentleAiDe
812
872
  for (const toolName of Object.keys(TOOL_CREATORS) as RegisteredToolName[]) {
813
873
  registerQuietTool(pi, toolName, resolveOverride, () => elapsedTiming);
814
874
  }
875
+ // Native Bash keeps pi's execution and schema; only its drawing becomes a
876
+ // Gentle card while the Card content preference is minimal, so quiet bash
877
+ // rows match the other tools. The resolver API is newer than this
878
+ // package's pinned pi types, so it is probed structurally; hosts without it
879
+ // — and the default content preference — keep pi's native bash rendering.
880
+ const host = pi as typeof pi & { registerToolRenderer?: (resolver: (toolName: string, next: () => unknown) => unknown) => void };
881
+ if (typeof host.registerToolRenderer === "function") {
882
+ host.registerToolRenderer((toolName, next) =>
883
+ toolName === "bash" && cardContent() === CARD_CONTENT.MINIMAL ? createQuietToolRenderer("bash", resolveOverride, () => elapsedTiming) : next());
884
+ }
815
885
  return registerCompactCodemode(pi);
816
886
  }