gentle-pi 3.7.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,101 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { USER_PI_HOME_ENV } from "../lib/gentle-shell-launcher.ts";
5
+ import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
6
+ import { withOverlayRepaint } from "../lib/overlay-repaint.ts";
7
+ import { createStatsLoader, currentSessionStats, type StatsLoader } from "../lib/stats-collector.ts";
8
+ import { StatsView } from "../lib/stats-view.ts";
9
+
10
+ // Gentle Stats: /gentle:stats opens a full-terminal panel over local Pi
11
+ // session history (tokens, cost, models, activity). It only reads the
12
+ // session files Pi already writes; nothing new is persisted.
13
+ //
14
+ // History spans two homes: the active one (Gentle Shell's isolated home) and
15
+ // the user's regular Pi home, which the launcher records before isolating.
16
+ // Launched without it (plain pi, or an older launcher), the conventional
17
+ // ~/.pi/agent stands in. The loader reads an alias of the same root once and
18
+ // counts a session copied into both homes once.
19
+
20
+ export const STATS_COMMAND_NAME = "gentle:stats";
21
+
22
+ /** Optional shortcut; there is no default key because the common alt+ keys are taken. */
23
+ export function statsViewKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
24
+ const value = env.GENTLE_PI_STATS_VIEW_KEY?.trim();
25
+ return !value || value.toLowerCase() === "off" ? undefined : value;
26
+ }
27
+
28
+ /** The sessions roots /gentle:stats reads: the active home and the user's original Pi home. */
29
+ export function statsSessionRoots(agentDir: string, env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string[] {
30
+ const userHome = env[USER_PI_HOME_ENV] || join(home, ".pi", "agent");
31
+ return [join(agentDir, "sessions"), join(userHome, "sessions")];
32
+ }
33
+
34
+ export interface GentleStatsDeps {
35
+ env?: NodeJS.ProcessEnv;
36
+ now?: () => number;
37
+ sessionsRoots?: () => readonly string[];
38
+ loader?: StatsLoader;
39
+ }
40
+
41
+ export default function gentleStats(pi: ExtensionAPI, deps: GentleStatsDeps = {}): void {
42
+ const env = deps.env ?? process.env;
43
+ const now = deps.now ?? (() => Date.now());
44
+ const sessionsRoots = deps.sessionsRoots ?? (() => statsSessionRoots(getAgentDir(), env));
45
+ // One loader per process keeps its per-file cache across openings.
46
+ const loader = deps.loader ?? createStatsLoader();
47
+ const overlays = new Set<StatsView>();
48
+
49
+ const openOverlay = async (ctx: ExtensionContext) => {
50
+ if (!ctx.hasUI) return;
51
+ if (ctx.mode !== "tui") {
52
+ ctx.ui.notify("The stats overlay requires TUI mode.", "warning");
53
+ return;
54
+ }
55
+ let view: StatsView | undefined;
56
+ await ctx.ui.custom<null>(
57
+ (tui, theme, _keybindings, done) => {
58
+ const close = withOverlayRepaint(tui, done);
59
+ view = new StatsView({
60
+ theme,
61
+ rows: () => Math.max(0, tui.terminal.rows),
62
+ cwd: ctx.sessionManager.getCwd(),
63
+ now,
64
+ load: () => loader.load(sessionsRoots()),
65
+ current: () => currentSessionStats(ctx.sessionManager.getEntries(), { sessionId: ctx.sessionManager.getSessionId() ?? "", now: now() }),
66
+ onClose: () => close(null),
67
+ requestRender: () => tui.requestRender(),
68
+ });
69
+ overlays.add(view);
70
+ // Open at once with a loading state; the scan repaints when it settles.
71
+ void view.load();
72
+ const interaction = createNativeFullscreenInteraction({
73
+ keyboardTarget: view,
74
+ requestRender: () => tui.requestRender(),
75
+ });
76
+ interaction.addChild(view);
77
+ return interaction;
78
+ },
79
+ { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 0, anchor: "center" } },
80
+ ).finally(() => {
81
+ view?.dispose();
82
+ if (view) overlays.delete(view);
83
+ });
84
+ };
85
+
86
+ pi.registerCommand(STATS_COMMAND_NAME, {
87
+ description: "Show local Pi usage stats: activity heatmap, tokens, cost, models, and this session. r cycles the range, s the project scope.",
88
+ handler: async (_args, ctx) => openOverlay(ctx),
89
+ });
90
+ const viewKey = statsViewKey(env);
91
+ if (viewKey) {
92
+ pi.registerShortcut(viewKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
93
+ description: "Show local Pi usage stats",
94
+ handler: async (ctx) => openOverlay(ctx),
95
+ });
96
+ }
97
+ pi.on("session_shutdown", () => {
98
+ for (const view of overlays) { view.handleInput("q"); view.dispose(); }
99
+ overlays.clear();
100
+ });
101
+ }
@@ -1,7 +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
+ import { appendSystemPromptOnce } from "../lib/append-system-prompt.ts";
3
4
  import { NativePointerRegion } from "../lib/native-pointer-region.ts";
4
- import { sidebarPart } from "../lib/shell-sidebar.ts";
5
+ import { panelHeaderRow } from "../lib/shell-card.ts";
6
+ import { sidebarPart, sidebarState } from "../lib/shell-sidebar.ts";
5
7
  import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
6
8
  import {
7
9
  applyTodo,
@@ -12,6 +14,7 @@ import {
12
14
  TODO_DETAILS_KEY,
13
15
  TODO_GLYPH,
14
16
  TODO_TOOL_NAME,
17
+ todoCardTone,
15
18
  todoPromptBlock,
16
19
  todoSummary,
17
20
  type TodoParams,
@@ -21,12 +24,16 @@ import {
21
24
  // Gentle Todo: the task list the model keeps while it works, drawn as a
22
25
  // Gentle Shell card above the editor. Three things keep it current that a
23
26
  // static tool description cannot: `write` replaces the whole list in one
24
- // call, every turn's system prompt carries the open tasks and the rules, and
27
+ // call, each run's system prompt carries the open tasks and the rules, and
25
28
  // a list that goes untouched while tasks stay open is marked stale for both
26
- // the human and the model.
29
+ // the human and the model. Long runs also receive one request-local reminder
30
+ // after several tool-use turns, without waking the agent or changing tasks.
27
31
 
28
32
  const WIDGET_KEY = "gentle-todo";
33
+ const STATUS_ENUM = ["pending", "in_progress", "blocked", "done", "dropped"];
29
34
  const COLLAPSE_KEY_DEFAULT = "ctrl+shift+t";
35
+ const REMIND_AFTER_TOOL_TURNS = 4;
36
+ 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.";
30
37
  const TOOL_PARAMETERS = {
31
38
  type: "object",
32
39
  additionalProperties: false,
@@ -43,15 +50,15 @@ const TOOL_PARAMETERS = {
43
50
  properties: {
44
51
  id: { type: "integer", description: "Existing task id to keep." },
45
52
  title: { type: "string", description: "Short imperative title, e.g. 'Write the parser'." },
46
- status: { type: "string", enum: ["pending", "in_progress", "done"], description: "Defaults to pending." },
47
- note: { type: "string", description: "What is happening right now, shown while in_progress, e.g. 'writing tests'." },
53
+ 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." },
54
+ 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'." },
48
55
  },
49
56
  },
50
57
  },
51
58
  id: { type: "integer", description: "Task id for update." },
52
59
  title: { type: "string", description: "Title for add, or a new title for update." },
53
- status: { type: "string", enum: ["pending", "in_progress", "done"], description: "Status for add or update." },
54
- note: { type: "string", description: "Note for add or update." },
60
+ status: { type: "string", enum: STATUS_ENUM, description: "Status for add or update." },
61
+ note: { type: "string", description: "Note for add or update; required for blocked." },
55
62
  },
56
63
  } as const;
57
64
 
@@ -70,12 +77,18 @@ export function todoCollapseKey(env: NodeJS.ProcessEnv = process.env): string |
70
77
  interface TodoSession {
71
78
  state: TodoState;
72
79
  turn: number;
80
+ /** Separate from the existing run-based card freshness. */
81
+ toolTurnsSinceWrite: number;
82
+ reminded: boolean;
83
+ wroteThisTurn: boolean;
73
84
  collapsed: boolean;
74
85
  /** A finished list stays on screen for the turn it finished in, then clears. */
75
86
  clearOnNextTurn: boolean;
76
87
  ui: ExtensionContext["ui"] | undefined;
77
88
  host: { requestRender(): void } | undefined;
78
89
  tui: TUI | undefined;
90
+ /** Resolve the visible host when the key is pressed, not when it renders. */
91
+ scrollTodo?: (lines: number) => void;
79
92
  }
80
93
 
81
94
  function sessionKey(ctx: ExtensionContext): string {
@@ -91,7 +104,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
91
104
  const key = sessionKey(ctx);
92
105
  let current = sessions.get(key);
93
106
  if (!current) {
94
- current = { state: emptyTodo(), turn: 0, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
107
+ current = { state: emptyTodo(), turn: 0, toolTurnsSinceWrite: 0, reminded: false, wroteThisTurn: false, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
95
108
  sessions.set(key, current);
96
109
  }
97
110
  return current;
@@ -103,18 +116,51 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
103
116
  current.host?.requestRender();
104
117
  };
105
118
 
106
- const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1], scrollable: boolean, spacer: boolean): Component & { dispose(): void } => {
119
+ const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1], spacer: boolean): Component & { dispose(): void; digest(): string; scrollBy(lines: number): void } => {
107
120
  let hovered = false;
121
+ let body: string[] = [];
122
+ let collapsed = current.collapsed;
123
+ // Widgets are measured as leaves, so native layout cannot assign this
124
+ // body a viewport. Use native scroll state and explicitly slice its lines
125
+ // while keeping the collapse header and card footer outside the viewport.
126
+ const scroll = new ScrollView({ render: () => body, invalidate() {} }, { follow: "none", overscroll: "contain" });
127
+ const scrollBy = (lines: number) => {
128
+ if (current.collapsed) return;
129
+ scroll.scrollBy(lines);
130
+ if (current.tui) invalidateSidebar(current.tui);
131
+ current.host?.requestRender();
132
+ };
133
+ // The row the rendered card draws its header on: 0 for the outlined
134
+ // frame, 1 below the float panel's top padding row.
135
+ let headerRow = 0;
108
136
  const card: Component = {
109
137
  render(width: number) {
138
+ const stale = staleTurns(current.state, current.turn);
139
+ headerRow = panelHeaderRow(theme, width, todoCardTone(stale));
110
140
  const lines = renderTodoCard(current.state, theme, width, {
111
141
  collapsed: current.collapsed,
112
- staleTurns: staleTurns(current.state, current.turn),
142
+ staleTurns: stale,
113
143
  collapseKey,
114
144
  hovered,
115
- ...(scrollable ? { scrollable: true } : {}),
145
+ scrollable: true,
116
146
  });
117
- return spacer && lines.length > 0 ? [...lines, ""] : lines;
147
+ if (collapsed !== current.collapsed) {
148
+ collapsed = current.collapsed;
149
+ scroll.scrollToStart();
150
+ }
151
+ // At most one third of the screen, capped at 16 rows. Read height
152
+ // on every render so a mobile resize immediately releases chat space.
153
+ const terminalRows = current.tui?.terminal?.rows ?? 48;
154
+ const height = Math.min(16, Math.floor(terminalRows / 3));
155
+ const bodyStart = headerRow === 1 ? 3 : 1;
156
+ body = lines.slice(bodyStart, -1);
157
+ const overflow = !current.collapsed && lines.length + Number(spacer) > height;
158
+ const room = Math.max(1, height - bodyStart - 1 - Number(spacer) - Number(overflow));
159
+ scroll.updateLayout(body.length, current.collapsed ? body.length : room, () => current.host?.requestRender());
160
+ const visible = scroll.render(width).slice(scroll.scrollTop, scroll.scrollTop + scroll.viewportHeight);
161
+ const output = [...lines.slice(0, bodyStart), ...visible, ...lines.slice(-1)];
162
+ if (overflow) output.push(theme.fg("muted", truncateToWidth(`↑↓ ${scroll.scrollTop + 1}–${scroll.scrollTop + visible.length}/${body.length} · ctrl+shift+↑/↓`, width)));
163
+ return spacer && output.length > 0 ? [...output, ""] : output;
118
164
  },
119
165
  invalidate() {
120
166
  hovered = false;
@@ -122,10 +168,10 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
122
168
  };
123
169
  const region = new NativePointerRegion(card, {
124
170
  onHover(event) {
125
- // The region spans the whole card, but only the header row (y===0)
126
- // is the clickable control, so a move elsewhere in the card clears
127
- // hover exactly like leaving the region entirely would.
128
- const next = event.y === 0;
171
+ // The region spans the whole card, but only the header row is the
172
+ // clickable control, so a move elsewhere in the card clears hover
173
+ // exactly like leaving the region entirely would.
174
+ const next = event.y === headerRow;
129
175
  if (next === hovered) return { handled: true };
130
176
  hovered = next;
131
177
  return { handled: true, render: true };
@@ -136,12 +182,21 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
136
182
  current.host?.requestRender();
137
183
  },
138
184
  onClick(event) {
139
- if (event.button !== "left" || event.y !== 0) return undefined;
185
+ if (event.button !== "left" || event.y !== headerRow) return undefined;
140
186
  toggle(current);
141
187
  return { handled: true, render: true };
142
188
  },
189
+ onWheel(event) {
190
+ if (current.collapsed || body.length <= scroll.viewportHeight) return undefined;
191
+ scrollBy(event.wheelDelta ?? 0);
192
+ return { handled: true, render: true };
193
+ },
143
194
  });
144
195
  return {
196
+ scrollBy,
197
+ // Sidebar caches must observe height-only resizes and local viewport
198
+ // changes, even when terminal width and task content stay the same.
199
+ digest: () => JSON.stringify([current.tui?.terminal?.rows, current.collapsed, current.turn, hovered, scroll.scrollTop]),
145
200
  render: (width) => region.render(width),
146
201
  handleMouse: (event) => region.handleMouse(event),
147
202
  invalidate: () => region.invalidate(),
@@ -153,6 +208,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
153
208
  if (current.tui) invalidateSidebar(current.tui);
154
209
  if (!current.ui) return;
155
210
  if (current.state.tasks.length === 0) {
211
+ current.scrollTodo = undefined;
156
212
  current.ui.setWidget(WIDGET_KEY, undefined);
157
213
  return;
158
214
  }
@@ -160,7 +216,15 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
160
216
  current.ui.setWidget(WIDGET_KEY, (tui, theme) => {
161
217
  snapshot.host = tui;
162
218
  snapshot.tui = tui;
163
- return sidebarPart(tui, "todo", todoCard(snapshot, theme, false, true), todoCard(snapshot, theme, true, false));
219
+ const bottom = todoCard(snapshot, theme, true);
220
+ const rail = todoCard(snapshot, theme, false);
221
+ snapshot.scrollTodo = (lines) => {
222
+ // Cached rail lines do not re-render when a narrow→wide resize
223
+ // restores the sidebar. Select ownership from the live layout.
224
+ const state = tui.terminal ? sidebarState(tui) : undefined;
225
+ (state?.active && state.ownsHost?.() ? rail : bottom).scrollBy(lines);
226
+ };
227
+ return sidebarPart(tui, "todo", bottom, rail);
164
228
  });
165
229
  };
166
230
 
@@ -175,6 +239,8 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
175
239
  "Mark a task in_progress before starting it and done right after finishing it; keep exactly one task in_progress.",
176
240
  "Prefer write with the complete list whenever the plan changes; keep ids of tasks that already exist.",
177
241
  "Never mark a task done while tests fail or the work is partial; add a task for the blocker instead.",
242
+ "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.",
243
+ "When the plan changes, mark tasks it made obsolete dropped, never done; dropped tasks no longer count as open.",
178
244
  ],
179
245
  parameters: TOOL_PARAMETERS,
180
246
  executionMode: "sequential",
@@ -192,6 +258,11 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
192
258
  if (!result.error) {
193
259
  current.state = result.state;
194
260
  current.clearOnNextTurn = false;
261
+ if (params.action !== "list") {
262
+ current.toolTurnsSinceWrite = 0;
263
+ current.reminded = false;
264
+ current.wroteThisTurn = true;
265
+ }
195
266
  if (current.tui) invalidateSidebar(current.tui);
196
267
  }
197
268
  return {
@@ -210,6 +281,13 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
210
281
  });
211
282
  }
212
283
 
284
+ for (const [key, lines] of [["ctrl+shift+up", -3], ["ctrl+shift+down", 3]] as const) {
285
+ pi.registerShortcut(key, {
286
+ description: `Scroll the Todo list ${lines < 0 ? "up" : "down"}`,
287
+ handler: async (ctx) => session(ctx).scrollTodo?.(lines),
288
+ });
289
+ }
290
+
213
291
  pi.on("session_start", (_event, ctx) => {
214
292
  const current = session(ctx);
215
293
  // A list that was already finished when the session was left is history,
@@ -217,6 +295,9 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
217
295
  const replayed = replayTodo(ctx.sessionManager.getBranch());
218
296
  current.state = replayed.tasks.length > 0 && todoSummary(replayed).open === 0 ? { ...replayed, tasks: [] } : replayed;
219
297
  current.turn = ctx.sessionManager.getBranch().filter((entry) => (entry as { type?: string }).type === "message" && (entry as { message?: { role?: string } }).message?.role === "user").length;
298
+ current.toolTurnsSinceWrite = 0;
299
+ current.reminded = false;
300
+ current.wroteThisTurn = false;
220
301
  current.ui = ctx.hasUI ? ctx.ui : undefined;
221
302
  show(current);
222
303
  });
@@ -236,7 +317,32 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
236
317
  }
237
318
  const block = todoPromptBlock(current.state, staleTurns(current.state, current.turn));
238
319
  if (!block) return undefined;
239
- return { systemPrompt: `${event.systemPrompt}\n\n${block}` };
320
+ // gentle-shell#1485: pi-claude-bridge drops a handler-returned
321
+ // systemPrompt, so the open-tasks block goes through appendSystemPrompt.
322
+ appendSystemPromptOnce(event.systemPromptOptions, block);
323
+ return undefined;
324
+ });
325
+
326
+ pi.on("turn_end", (event, ctx) => {
327
+ const current = session(ctx);
328
+ // A turn containing a valid todo write is already reconciled. Failed
329
+ // writes and list reads deliberately do not refresh this counter.
330
+ if (!current.wroteThisTurn && event.message.role === "assistant" && event.message.content.some((part) => part.type === "toolCall")) {
331
+ current.toolTurnsSinceWrite += 1;
332
+ }
333
+ current.wroteThisTurn = false;
334
+ });
335
+
336
+ pi.on("context", (event, ctx) => {
337
+ const current = session(ctx);
338
+ if (current.reminded || current.toolTurnsSinceWrite < REMIND_AFTER_TOOL_TURNS) return undefined;
339
+ if (!current.state.tasks.some((task) => task.status === "pending" || task.status === "in_progress")) return undefined;
340
+ current.reminded = true;
341
+ // Context transforms are request-local: no transcript entry, UI
342
+ // notification, continuation or provider-specific wake is needed.
343
+ return {
344
+ messages: [...event.messages, { role: "custom" as const, customType: "gentle-todo-reminder", content: TODO_REMINDER, display: false, timestamp: Date.now() }],
345
+ };
240
346
  });
241
347
 
242
348
  pi.on("tool_execution_end", (event, ctx) => {
@@ -0,0 +1,38 @@
1
+ import path from "node:path";
2
+ // SPDX-FileCopyrightText: 2026 ExoPro. Inspired by @jasonish/pi-prompt-history
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ import fs from "node:fs";
6
+
7
+ /**
8
+ * Shared atomic JSON writer (design §D3): serialize to a `.tmp` file in the
9
+ * SAME directory as the target, then renameSync it into place — a same-dir
10
+ * rename is atomic on POSIX/APFS, so readers see the old or the new file,
11
+ * never a partial write. Any error returns false and never throws.
12
+ *
13
+ * No fsync: both consumers treat lost writes as derived cache (a lost index
14
+ * rebuilds on the next open; a lost tombstone resurfaces a prompt the user
15
+ * can re-delete), so the per-write fsync cost is not justified — the crash
16
+ * window is documented, not fixed. The staging name is unique per write
17
+ * (`.tmp-<pid>-<ts>`, the same convention as the store.ts writers): the
18
+ * state dir is shared across concurrent pi instances, so a fixed
19
+ * `${filePath}.tmp` would let two writers clobber the same staging file
20
+ * (torn target JSON, spurious rename failures). A failed write unlinks its
21
+ * staging file, so orphaned `.tmp` files do not accumulate.
22
+ */
23
+ export function writeJsonAtomic(filePath: string, value: unknown): boolean {
24
+ const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
25
+ try {
26
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
27
+ fs.writeFileSync(tmpPath, JSON.stringify(value), "utf8");
28
+ fs.renameSync(tmpPath, filePath);
29
+ return true;
30
+ } catch {
31
+ try {
32
+ fs.unlinkSync(tmpPath);
33
+ } catch {
34
+ // staging file never created or already renamed
35
+ }
36
+ return false;
37
+ }
38
+ }
@@ -0,0 +1,183 @@
1
+ // SPDX-FileCopyrightText: 2026 ExoPro. Inspired by @jasonish/pi-prompt-history
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { createHash } from "node:crypto";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { writeJsonAtomic } from "./atomic-write.ts";
8
+ import { promptDedupKey } from "./selector-helpers.ts";
9
+
10
+ /** Name of the tombstone file inside the injected state dir (spec C4). */
11
+ const HIDE_FILE_NAME = "hidden.json";
12
+
13
+ /** Marks tombstone entries written in the exact (hashed) key format. */
14
+ const TOMBSTONE_KEY_PREFIX = "sha256:";
15
+
16
+ /**
17
+ * UI-level prompt identity: whitespace-collapsed, trimmed, case-insensitive,
18
+ * never truncated. The store's scope deletes sweep by this same identity,
19
+ * so a tombstone hides exactly the copies a delete removes.
20
+ */
21
+ export function promptIdentity(text: string): string {
22
+ return text.replace(/\s+/g, " ").trim().toLowerCase();
23
+ }
24
+
25
+ /**
26
+ * Tombstone key for `text`: a SHA-256 of its full prompt identity. Exact —
27
+ * two prompts that merely share a prefix get different keys — and hashed,
28
+ * so hidden.json never holds the text of a deleted prompt.
29
+ */
30
+ export function tombstoneKey(text: string): string {
31
+ return (
32
+ TOMBSTONE_KEY_PREFIX +
33
+ createHash("sha256").update(promptIdentity(text)).digest("hex")
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Whether `text` is hidden by the tombstone set `keys`. Hashed keys match
39
+ * the exact prompt identity. Plaintext entries from the earlier prefix
40
+ * format (`promptDedupKey`: the first 120 normalized characters) stay
41
+ * honored as written, so upgrading never resurfaces a hidden prompt; only
42
+ * new deletions use the exact format.
43
+ */
44
+ export function isPromptHidden(keys: ReadonlySet<string>, text: string): boolean {
45
+ if (keys.size === 0) return false;
46
+ return keys.has(tombstoneKey(text)) || keys.has(promptDedupKey(text));
47
+ }
48
+
49
+ /**
50
+ * Retention cap for hidden.json (slice-05 D5): the tombstone file is a
51
+ * rebuildable derived cache, not a retention guarantee, so it holds at
52
+ * most this many keys in recency order; hiding past the cap drops the
53
+ * OLDEST keys from the front.
54
+ */
55
+ export const HIDE_FILE_MAX_ENTRIES = 1000;
56
+
57
+ /**
58
+ * Shared recovery warning for a file that exists but cannot be trusted
59
+ * (spec C4, fail-closed READ half): toast-suitable, names hidden.json, and
60
+ * gives the user the explicit restore-or-delete choice.
61
+ */
62
+ const RECOVERY_MESSAGE =
63
+ "The prompt-history hide list (hidden.json) is corrupt or unreadable. History is blocked until you restore the file or delete it (hidden prompts may then reappear).";
64
+
65
+ /**
66
+ * Result of one tombstone write (spec C4): `written` on a successful atomic
67
+ * write, or an error object carrying a short, toast-suitable reason. Never
68
+ * throws.
69
+ */
70
+ export type HideResult =
71
+ | { status: "written" }
72
+ | { status: "error"; message: string };
73
+
74
+ /**
75
+ * Result of one tombstone read (spec C4): `trusted` keys when the file is
76
+ * missing or holds a valid array, or `untrusted` when the file exists but
77
+ * cannot be trusted. History reads FAIL CLOSED on `untrusted`: callers must
78
+ * block the drain instead of emptying the tombstone set, because hidden
79
+ * prompts may contain secrets an empty set would resurface.
80
+ */
81
+ export type HiddenRead =
82
+ | { status: "trusted"; keys: Set<string> }
83
+ | {
84
+ status: "untrusted";
85
+ reason: "unreadable" | "corrupt" | "malformed";
86
+ message: string;
87
+ };
88
+
89
+ /**
90
+ * Read the tombstone key set from `stateDir/hidden.json` — the READ half of
91
+ * the hide-file contract (spec C4). Fail-closed for history: a file that
92
+ * exists but is unreadable, corrupt, or wrong-shaped returns `untrusted`
93
+ * with the recovery warning so callers block the drain; it never degrades
94
+ * to an empty trusted set. A MISSING file — before any deletion — is the
95
+ * safe empty case and reads `trusted` with no keys. A valid array is
96
+ * trusted; junk items inside it are ignored, never trusted. Keys are the
97
+ * `tombstoneKey` hashes written by `hidePrompt`, or plaintext entries from
98
+ * the earlier prefix format (see `isPromptHidden`); the call never throws.
99
+ * A valid array's stored order is preserved (the recency order — oldest
100
+ * first — that `hidePrompt` maintains and caps).
101
+ */
102
+ export function readHiddenPrompts(stateDir: string): HiddenRead {
103
+ let raw: string;
104
+ try {
105
+ raw = fs.readFileSync(path.join(stateDir, HIDE_FILE_NAME), "utf8");
106
+ } catch (error) {
107
+ const code = (error as { code?: unknown } | null | undefined)?.code;
108
+ if (code === "ENOENT") {
109
+ // Missing before any deletion: the safe empty tombstone set.
110
+ return { status: "trusted", keys: new Set<string>() };
111
+ }
112
+ return {
113
+ status: "untrusted",
114
+ reason: "unreadable",
115
+ message: RECOVERY_MESSAGE,
116
+ };
117
+ }
118
+ let parsed: unknown;
119
+ try {
120
+ parsed = JSON.parse(raw);
121
+ } catch {
122
+ return { status: "untrusted", reason: "corrupt", message: RECOVERY_MESSAGE };
123
+ }
124
+ const keys = new Set<string>();
125
+ if (!Array.isArray(parsed)) {
126
+ return {
127
+ status: "untrusted",
128
+ reason: "malformed",
129
+ message: RECOVERY_MESSAGE,
130
+ };
131
+ }
132
+ for (const item of parsed) {
133
+ if (typeof item === "string" && item !== "") keys.add(item);
134
+ }
135
+ return { status: "trusted", keys };
136
+ }
137
+
138
+ /**
139
+ * Write the tombstone key for `text` into `stateDir/hidden.json` — the
140
+ * WRITE half of the hide-file contract (spec C4). The key is the shared
141
+ * `tombstoneKey` (byte-match normative with the drain and seed filters via
142
+ * `isPromptHidden` — never a re-implementation). The file array is RECENCY-ordered — oldest key
143
+ * first, newest key appended last — and re-hiding an existing key
144
+ * refreshes it to the end (delete + add, since Set.add on a present
145
+ * member keeps its old position). The file is capped at
146
+ * `HIDE_FILE_MAX_ENTRIES` (1000): after the append, keys drop from the
147
+ * FRONT until the file fits, so hidden.json stays a bounded cache — a
148
+ * dropped (oldest) prompt may reappear in the list and can be deleted
149
+ * again. Keys persist in that insertion order — NO sort — via the shared
150
+ * atomic tmp+rename writer. An untrusted existing file is never silently
151
+ * reset (a clean rewrite would clear the blocked state one hide later):
152
+ * hidePrompt refuses with the recovery warning until the user restores or
153
+ * deletes the file. A missing file is the clean baseline; any write
154
+ * failure returns an error object for the delete-flow toast; the call
155
+ * never throws.
156
+ */
157
+ export function hidePrompt(stateDir: string, text: string): HideResult {
158
+ const read = readHiddenPrompts(stateDir);
159
+ if (read.status === "untrusted") {
160
+ // Refuse without writing: never reset the untrusted state silently.
161
+ return { status: "error", message: read.message };
162
+ }
163
+ // Recency order (slice-05 D5): the set iterates in stored file order
164
+ // (oldest first); delete+add refreshes a re-hidden key to the END.
165
+ const key = tombstoneKey(text);
166
+ read.keys.delete(key);
167
+ read.keys.add(key);
168
+ // Cap: drop the OLDEST keys from the front once over the limit.
169
+ const ordered = [...read.keys];
170
+ if (ordered.length > HIDE_FILE_MAX_ENTRIES) {
171
+ ordered.splice(0, ordered.length - HIDE_FILE_MAX_ENTRIES);
172
+ }
173
+ const written = writeJsonAtomic(
174
+ path.join(stateDir, HIDE_FILE_NAME),
175
+ ordered,
176
+ );
177
+ return written
178
+ ? { status: "written" }
179
+ : {
180
+ status: "error",
181
+ message: "Could not write the hide file; the prompt may reappear.",
182
+ };
183
+ }