gentle-pi 4.0.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 (398) hide show
  1. package/README.md +122 -8
  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 +703 -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/bundled-install.md +397 -0
  28. package/docs/delegated-verification.md +3 -5
  29. package/docs/gentle-agents-activity.md +513 -0
  30. package/docs/gentle-shell-uninstall.md +136 -0
  31. package/docs/gentle-shell.md +81 -31
  32. package/docs/install-wizard.md +2040 -0
  33. package/docs/native-authority-architecture.md +1 -1
  34. package/docs/permission-modes.md +82 -0
  35. package/docs/prompt-history.md +1 -1
  36. package/docs/readme-reference.md +267 -40
  37. package/docs/render-cache-performance.md +28 -0
  38. package/docs/sound-notifications-proposal.md +191 -0
  39. package/docs/sound-notifications.md +114 -0
  40. package/docs/yolo-mode.md +9 -9
  41. package/extensions/child-context.ts +11 -2
  42. package/extensions/child-safety.ts +4 -2
  43. package/extensions/codegraph-tools.ts +18 -5
  44. package/extensions/gentle-agents.ts +688 -88
  45. package/extensions/gentle-ai.ts +886 -238
  46. package/extensions/gentle-herdr-activity.ts +81 -0
  47. package/extensions/gentle-notifications.ts +104 -0
  48. package/extensions/gentle-shell.ts +265 -69
  49. package/extensions/gentle-todo.ts +108 -13
  50. package/extensions/history/index.ts +11 -6
  51. package/extensions/pi-pretty.ts +39 -3
  52. package/extensions/quiet-tools.ts +91 -21
  53. package/extensions/skill-registry.ts +175 -46
  54. package/extensions/startup-banner.ts +11 -2
  55. package/lib/agent-home.ts +10 -1
  56. package/lib/agent-model-resolution.ts +80 -0
  57. package/lib/agent-profiles.ts +13 -3
  58. package/lib/agents-completion-delivery.ts +11 -3
  59. package/lib/agents-messaging.ts +1 -1
  60. package/lib/agents-protocol.ts +40 -1
  61. package/lib/agents-runner.ts +90 -15
  62. package/lib/auto-permission-classifier.ts +280 -0
  63. package/lib/auto-permission-runtime.ts +268 -0
  64. package/lib/background-jobs-tools.ts +202 -0
  65. package/lib/background-jobs.ts +253 -0
  66. package/lib/background-monitor.ts +154 -0
  67. package/lib/bounded-writer-admission.ts +83 -10
  68. package/lib/builtin-codemode-optout.ts +166 -0
  69. package/lib/card-content-policy.ts +85 -0
  70. package/lib/child-package-injection.ts +62 -0
  71. package/lib/codemode-renderer.ts +14 -4
  72. package/lib/command-palette-catalog.ts +3 -2
  73. package/lib/context-threshold.ts +26 -0
  74. package/lib/gentle-ai-binary.ts +109 -4
  75. package/lib/gentle-shell-bundled.ts +517 -0
  76. package/lib/gentle-shell-launcher.ts +74 -23
  77. package/lib/gentle-shell-startup-flow.ts +54 -0
  78. package/lib/gentle-shell-startup-update.ts +92 -0
  79. package/lib/gentle-shell-uninstall.ts +840 -0
  80. package/lib/gentle-shell-update-panel.ts +231 -0
  81. package/lib/herdr-activity.ts +183 -0
  82. package/lib/inprocess-reviewer.ts +3 -0
  83. package/lib/jobs-sidebar-state.ts +20 -0
  84. package/lib/jobs-view.ts +161 -0
  85. package/lib/model-price-ratio.ts +83 -0
  86. package/lib/nan-provider.ts +33 -11
  87. package/lib/native-review-cli.ts +194 -73
  88. package/lib/notification-audio-native.ts +162 -0
  89. package/lib/notification-audio-windows.ts +399 -0
  90. package/lib/notification-audio.ts +332 -0
  91. package/lib/notification-customize.ts +241 -0
  92. package/lib/notification-events.ts +113 -0
  93. package/lib/notification-policy.ts +148 -0
  94. package/lib/notification-pulse-client.ts +367 -0
  95. package/lib/notification-pulse-protocol.ts +305 -0
  96. package/lib/notification-pulse-stream.ts +213 -0
  97. package/lib/notification-pulse-worker.ts +66 -0
  98. package/lib/notification-scheduler.ts +173 -0
  99. package/lib/notification-service.ts +202 -0
  100. package/lib/notification-ui.ts +87 -0
  101. package/lib/odd-phase.ts +1 -1
  102. package/lib/orchestrator-alias.ts +20 -0
  103. package/lib/orchestrator-catalog.ts +79 -0
  104. package/lib/orchestrator-consultation.ts +61 -0
  105. package/lib/orchestrator-discovery.ts +49 -0
  106. package/lib/orchestrator-helper-consent.ts +122 -0
  107. package/lib/orchestrator-helper.ts +167 -0
  108. package/lib/orchestrator-message-card.ts +105 -0
  109. package/lib/orchestrator-presence.ts +115 -14
  110. package/lib/orchestrator-scope.ts +80 -0
  111. package/lib/orchestrator-state.ts +107 -0
  112. package/lib/orchestrator-tool-card.ts +280 -0
  113. package/lib/orchestrator-work-search.ts +218 -0
  114. package/lib/orchestrator-work.ts +79 -0
  115. package/lib/permission-mode-policy.ts +59 -0
  116. package/lib/review-candidate-view-owner.ts +8 -6
  117. package/lib/review-candidate-view.ts +3 -2
  118. package/lib/review-compact-contract.ts +2 -2
  119. package/lib/review-host-relay.ts +7 -2
  120. package/lib/review-integration-v2.ts +100 -22
  121. package/lib/review-publication-gate.ts +1 -1
  122. package/lib/review-risk-assessment.ts +77 -51
  123. package/lib/review-sidebar-state.ts +2 -1
  124. package/lib/review-transaction.ts +72 -3
  125. package/lib/runtime-metrics-children.ts +1 -1
  126. package/lib/runtime-metrics-native.ts +3 -2
  127. package/lib/selection-engine.ts +18 -6
  128. package/lib/session-change-capture.ts +5 -4
  129. package/lib/session-changes.ts +27 -22
  130. package/lib/session-profile-binding.ts +83 -0
  131. package/lib/session-profile-freeze.ts +266 -0
  132. package/lib/session-worktree-registry.ts +74 -8
  133. package/lib/shell-bar.ts +41 -11
  134. package/lib/shell-changes-view.ts +40 -13
  135. package/lib/shell-changes.ts +86 -71
  136. package/lib/shell-sidebar-layout.ts +5 -1
  137. package/lib/shell-todo.ts +62 -19
  138. package/lib/shell-usage.ts +4 -3
  139. package/lib/status-timing-diagnostics.ts +201 -0
  140. package/lib/telemetry-trigger.ts +3 -1
  141. package/lib/vim-editor-adapter.ts +7 -3
  142. package/lib/visual-customization-policy.ts +20 -9
  143. package/lib/visual-customize-view.ts +171 -20
  144. package/lib/writer-surfaces.ts +162 -0
  145. package/lib/yolo-session-policy.ts +198 -121
  146. package/package.json +1 -1
  147. package/runtime/child-package-injection.mjs +63 -0
  148. package/runtime/gentle-ai-binary.mjs +109 -4
  149. package/runtime/gentle-shell-bundled.mjs +518 -0
  150. package/runtime/gentle-shell-launcher.mjs +73 -22
  151. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  152. package/runtime/gentle-shell-startup-update.mjs +93 -0
  153. package/runtime/gentle-shell-uninstall.mjs +841 -0
  154. package/runtime/gentle-shell-update-panel.mjs +232 -0
  155. package/runtime/native-review-cli.mjs +191 -70
  156. package/runtime/review-integration-v2.mjs +100 -22
  157. package/runtime/review-risk-assessment.mjs +77 -51
  158. package/runtime/status-timing-diagnostics.mjs +202 -0
  159. package/runtime/telemetry-trigger.mjs +3 -1
  160. package/scripts/bootstrap.cmd +255 -0
  161. package/scripts/bootstrap.sh +168 -0
  162. package/scripts/build-distribution.mjs +157 -0
  163. package/scripts/build-installer-bundles.mjs +136 -0
  164. package/scripts/build-runtime-modules.mjs +7 -0
  165. package/scripts/bundled-install.mjs +1308 -0
  166. package/scripts/bundled-wizard.mjs +399 -0
  167. package/scripts/check-types.mjs +4 -6
  168. package/scripts/gentle-ai-installer.mjs +79 -32
  169. package/scripts/install-gentle-ai.mjs +48 -1
  170. package/scripts/install-tui-mode-setting.mjs +10 -1
  171. package/scripts/install-wizard-preview.mjs +154 -0
  172. package/scripts/installer-downloads.mjs +648 -0
  173. package/scripts/installer-preflight.mjs +387 -0
  174. package/scripts/installer-probes.mjs +564 -0
  175. package/scripts/installer-runner.mjs +1102 -0
  176. package/scripts/installer-server.mjs +881 -0
  177. package/scripts/installer-windows-artifacts.json +15 -0
  178. package/scripts/installer-windows.mjs +752 -0
  179. package/scripts/main-channel.mjs +452 -0
  180. package/scripts/npm-pack-result.mjs +31 -0
  181. package/scripts/render-cache-performance-worker.mjs +205 -0
  182. package/scripts/render-cache-performance.mjs +115 -0
  183. package/scripts/run-test-suite.mjs +22 -8
  184. package/scripts/test-installer-acceptance.sh +161 -0
  185. package/scripts/test-packed-runner.mjs +23 -5
  186. package/scripts/types-baseline.json +2 -92
  187. package/scripts/verify-package-files.mjs +82 -18
  188. package/skills/gentle-ai/SKILL.md +12 -11
  189. package/tests/agent-home.test.ts +56 -3
  190. package/tests/agent-model-resolution.test.ts +113 -0
  191. package/tests/agent-profiles.test.ts +12 -0
  192. package/tests/agent-risk-escalation.test.ts +123 -0
  193. package/tests/agents-completion-delivery.test.ts +17 -0
  194. package/tests/agents-protocol.test.ts +57 -0
  195. package/tests/agents-queries.test.ts +9 -9
  196. package/tests/agents-runner-process.test.ts +31 -0
  197. package/tests/agents-runner.test.ts +89 -4
  198. package/tests/auto-mode-invariants.test.ts +258 -0
  199. package/tests/auto-mode-sdk.test.ts +57 -0
  200. package/tests/auto-mode.test.ts +49 -0
  201. package/tests/auto-permission-classifier.test.ts +283 -0
  202. package/tests/auto-permission-runtime.test.ts +196 -0
  203. package/tests/background-cache-warming.test.ts +2 -1
  204. package/tests/background-jobs.test.ts +227 -0
  205. package/tests/background-monitor.test.ts +195 -0
  206. package/tests/background-subagents-default-mode.test.ts +28 -5
  207. package/tests/bounded-stop-contract.test.ts +85 -0
  208. package/tests/bounded-writer-admission.test.ts +52 -1
  209. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  210. package/tests/bridge-wake-rendering.test.ts +56 -0
  211. package/tests/build-distribution.test.ts +269 -0
  212. package/tests/builtin-codemode-optout.test.ts +242 -0
  213. package/tests/bundled-install.test.ts +1319 -0
  214. package/tests/bundled-wizard.test.ts +505 -0
  215. package/tests/card-content-policy.test.ts +68 -0
  216. package/tests/card-style-policy.test.ts +34 -2
  217. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  218. package/tests/child-package-entrypoints.test.ts +65 -0
  219. package/tests/child-package-injection.test.ts +74 -0
  220. package/tests/child-safety.test.ts +12 -0
  221. package/tests/child-web-package.test.ts +224 -0
  222. package/tests/codegraph-tools.test.ts +35 -0
  223. package/tests/codemode-rendering.test.ts +107 -2
  224. package/tests/command-palette.test.ts +5 -5
  225. package/tests/context-threshold.test.ts +31 -0
  226. package/tests/delegate-for-reason-contract.test.ts +327 -0
  227. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  228. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  229. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  230. package/tests/engram-safety.test.ts +288 -0
  231. package/tests/generic-agent-tools.test.ts +16 -1
  232. package/tests/gentle-agents.test.ts +1722 -59
  233. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  234. package/tests/gentle-ai-binary.test.ts +19 -2
  235. package/tests/gentle-ai-child-guards.test.ts +202 -0
  236. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  237. package/tests/gentle-ai-installer.test.ts +548 -58
  238. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  239. package/tests/gentle-ai-renderer.test.ts +6 -6
  240. package/tests/gentle-ai.test.ts +999 -31
  241. package/tests/gentle-herdr-activity.test.ts +100 -0
  242. package/tests/gentle-notifications.test.ts +427 -0
  243. package/tests/gentle-shell-bin.test.ts +249 -5
  244. package/tests/gentle-shell-bundled.test.ts +1002 -0
  245. package/tests/gentle-shell-launcher.test.ts +234 -13
  246. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  247. package/tests/gentle-shell-startup-update.test.ts +85 -0
  248. package/tests/gentle-shell-uninstall.test.ts +978 -0
  249. package/tests/gentle-shell-update-panel.test.ts +158 -0
  250. package/tests/gentle-shell.test.ts +541 -96
  251. package/tests/gentle-todo.test.ts +253 -2
  252. package/tests/herdr-activity.test.ts +243 -0
  253. package/tests/history-agent-home.test.ts +81 -0
  254. package/tests/history-child-guard.test.ts +70 -0
  255. package/tests/inprocess-reviewer.test.ts +20 -0
  256. package/tests/install-gentle-ai-failure.test.ts +69 -0
  257. package/tests/install-tui-mode-setting.test.ts +15 -0
  258. package/tests/install-wizard.test.ts +1223 -0
  259. package/tests/installer-bundles.test.ts +122 -0
  260. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  261. package/tests/installer-preflight.test.ts +750 -0
  262. package/tests/installer-probes.test.ts +1106 -0
  263. package/tests/installer-runner.test.ts +2158 -0
  264. package/tests/installer-server.test.ts +1567 -0
  265. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  266. package/tests/jobs-view.test.ts +160 -0
  267. package/tests/main-channel-upgrade.test.ts +389 -0
  268. package/tests/main-channel.test.ts +344 -0
  269. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  270. package/tests/model-price-ratio.test.ts +116 -0
  271. package/tests/model-profiles-preservation.test.ts +153 -0
  272. package/tests/model-routing-package.test.ts +10 -0
  273. package/tests/nan-provider-runtime.test.ts +156 -0
  274. package/tests/nan-provider.test.ts +28 -18
  275. package/tests/native-binary-gate.test.ts +2 -0
  276. package/tests/native-review-capability-contract.test.ts +7 -1
  277. package/tests/native-review-cli.test.ts +236 -1
  278. package/tests/native-review-consent.test.ts +2 -2
  279. package/tests/native-review-parity-runtime.test.ts +1 -1
  280. package/tests/native-review-parity.test.ts +12 -48
  281. package/tests/native-risk-evidence-parity.test.ts +21 -0
  282. package/tests/no-self-pause-contract.test.ts +87 -0
  283. package/tests/notification-audio-native.test.ts +281 -0
  284. package/tests/notification-audio-windows.test.ts +581 -0
  285. package/tests/notification-audio.test.ts +548 -0
  286. package/tests/notification-customize.test.ts +655 -0
  287. package/tests/notification-events.test.ts +115 -0
  288. package/tests/notification-policy.test.ts +201 -0
  289. package/tests/notification-pulse-client.test.ts +356 -0
  290. package/tests/notification-pulse-protocol.test.ts +213 -0
  291. package/tests/notification-pulse-stream.test.ts +320 -0
  292. package/tests/notification-scheduler.test.ts +258 -0
  293. package/tests/notification-ui.test.ts +157 -0
  294. package/tests/npm-pack-result.test.ts +41 -0
  295. package/tests/odd-integration.test.ts +14 -5
  296. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  297. package/tests/odd-routing-contract.test.ts +80 -23
  298. package/tests/orchestrator-budget.test.ts +29 -6
  299. package/tests/orchestrator-catalog.test.ts +146 -0
  300. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  301. package/tests/orchestrator-consultation.test.ts +111 -0
  302. package/tests/orchestrator-discovery.test.ts +241 -0
  303. package/tests/orchestrator-helper-consent.test.ts +171 -0
  304. package/tests/orchestrator-helper.test.ts +297 -0
  305. package/tests/orchestrator-message-card.test.ts +190 -0
  306. package/tests/orchestrator-presence.test.ts +23 -0
  307. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  308. package/tests/orchestrator-scope.test.ts +107 -0
  309. package/tests/orchestrator-state.test.ts +114 -0
  310. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  311. package/tests/orchestrator-tool-card.test.ts +398 -0
  312. package/tests/orchestrator-work-search.test.ts +318 -0
  313. package/tests/orchestrator-work.test.ts +147 -0
  314. package/tests/package-manifest.test.ts +101 -17
  315. package/tests/packed-runner-owned-path.test.ts +35 -1
  316. package/tests/parallel-reason-contract.test.ts +21 -0
  317. package/tests/parallel-writer-admission.test.ts +210 -0
  318. package/tests/permission-mode-policy.test.ts +194 -0
  319. package/tests/persona-single-channel.test.ts +4 -12
  320. package/tests/pi-pretty.test.ts +56 -0
  321. package/tests/provider-defect-handoff.test.ts +4 -3
  322. package/tests/quiet-tool-rendering.test.ts +160 -3
  323. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  324. package/tests/render-cache-performance.test.ts +48 -0
  325. package/tests/review-agent-end-preflight.test.ts +62 -10
  326. package/tests/review-candidate-view.test.ts +101 -12
  327. package/tests/review-contract-prompt.test.ts +33 -4
  328. package/tests/review-controller-native-recovery.test.ts +327 -17
  329. package/tests/review-controller-native-routing.test.ts +255 -1
  330. package/tests/review-controller-workspace-root.test.ts +43 -2
  331. package/tests/review-controller.test.ts +2 -2
  332. package/tests/review-correction-lifecycle.test.ts +2 -1
  333. package/tests/review-gate.test.ts +7 -4
  334. package/tests/review-graph-schema.test.ts +2 -2
  335. package/tests/review-host-relay-routing.test.ts +5 -3
  336. package/tests/review-host-relay.test.ts +49 -2
  337. package/tests/review-integration-v2-forward.test.ts +21 -1
  338. package/tests/review-json-arguments.test.ts +167 -0
  339. package/tests/review-last-event-closure.test.ts +157 -8
  340. package/tests/review-ledger-contract.test.ts +5 -4
  341. package/tests/review-policy-judgment-day.test.ts +1 -1
  342. package/tests/review-policy-ordinary.test.ts +2 -2
  343. package/tests/review-relay-transport-agent.test.ts +66 -1
  344. package/tests/review-risk-assessment.test.ts +109 -33
  345. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  346. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  347. package/tests/review-test-fixtures.ts +7 -1
  348. package/tests/review-transaction.test.ts +18 -1
  349. package/tests/run-test-suite.test.ts +83 -5
  350. package/tests/runtime-harness.mjs +42 -15
  351. package/tests/runtime-metrics-extension.test.ts +1 -1
  352. package/tests/runtime-metrics-native.test.ts +31 -1
  353. package/tests/selection-engine.test.ts +30 -0
  354. package/tests/session-change-capture.test.ts +10 -10
  355. package/tests/session-changes-shell.test.ts +242 -48
  356. package/tests/session-changes.test.ts +17 -24
  357. package/tests/session-profile-binding.test.ts +163 -0
  358. package/tests/session-profile-freeze.test.ts +381 -0
  359. package/tests/session-subject-routing-contract.test.ts +34 -0
  360. package/tests/session-worktree-registry.test.ts +57 -2
  361. package/tests/shell-bar.test.ts +40 -0
  362. package/tests/shell-changes-view.test.ts +85 -34
  363. package/tests/shell-changes.test.ts +61 -62
  364. package/tests/shell-render-cache-runtime.test.ts +69 -0
  365. package/tests/shell-render-cache.test.ts +129 -0
  366. package/tests/shell-sidebar-layout.test.ts +27 -2
  367. package/tests/shell-todo.test.ts +120 -2
  368. package/tests/shell-usage-view.test.ts +1 -1
  369. package/tests/shell-usage.test.ts +35 -0
  370. package/tests/short-check-routing-contract.test.ts +49 -0
  371. package/tests/skill-registry-watcher.test.ts +119 -0
  372. package/tests/skill-registry.test.ts +311 -5
  373. package/tests/startup-banner.test.ts +117 -3
  374. package/tests/startup-home-repo.test.ts +170 -0
  375. package/tests/status-timing-diagnostics.test.ts +283 -0
  376. package/tests/support/orchestrator-modules.ts +19 -0
  377. package/tests/task-size-routing-contract.test.ts +241 -0
  378. package/tests/telemetry-trigger.test.ts +27 -1
  379. package/tests/test-strength-contract.test.ts +124 -0
  380. package/tests/verify-agent-contract.test.ts +90 -0
  381. package/tests/verify-first-launch-contract.test.ts +139 -0
  382. package/tests/verify-package-files.test.ts +56 -0
  383. package/tests/vim-editor-adapter.test.ts +7 -7
  384. package/tests/visual-customization-policy.test.ts +32 -3
  385. package/tests/visual-customize-view.test.ts +288 -10
  386. package/tests/writer-edit-surface-scope.test.ts +59 -2
  387. package/tests/writer-surfaces.test.ts +199 -0
  388. package/tests/yolo-customize.test.ts +18 -3
  389. package/tests/yolo-mode-runtime.test.ts +37 -10
  390. package/tests/yolo-mode.test.ts +32 -4
  391. package/tests/yolo-session-policy.test.ts +1 -1
  392. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  393. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  394. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  395. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  396. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  397. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  398. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
package/lib/shell-todo.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
1
+ import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
2
  import { CARD_TONE, panelHeaderRow, panelInnerWidth, renderCard, type CardTheme, type CardTone } from "./shell-card.ts";
3
3
  import { sanitizeTerminalText } from "./terminal-theme.ts";
4
4
  import { paintHoverable } from "./shell-hover.ts";
@@ -11,7 +11,11 @@ import { paintHoverable } from "./shell-hover.ts";
11
11
  export const TODO_STATUS = {
12
12
  PENDING: "pending",
13
13
  IN_PROGRESS: "in_progress",
14
+ /** Open, but waiting on something outside the list; always carries a note naming it. */
15
+ BLOCKED: "blocked",
14
16
  DONE: "done",
17
+ /** Will not be done, on purpose: terminal, and never counts as open. */
18
+ DROPPED: "dropped",
15
19
  } as const;
16
20
 
17
21
  export type TodoStatus = (typeof TODO_STATUS)[keyof typeof TODO_STATUS];
@@ -86,10 +90,20 @@ export interface TodoRenderOptions {
86
90
  export const TODO_DETAILS_KEY = "gentleTodo";
87
91
  export const TODO_TOOL_NAME = "todo";
88
92
  export const TODO_GLYPH = "❀";
89
- const STATUS_ALIASES: Record<string, TodoStatus> = { completed: TODO_STATUS.DONE, complete: TODO_STATUS.DONE, doing: TODO_STATUS.IN_PROGRESS, todo: TODO_STATUS.PENDING };
90
- const STATUS_GLYPH: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "○", [TODO_STATUS.IN_PROGRESS]: "◐", [TODO_STATUS.DONE]: "✓" };
91
- const STATUS_ROLE: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "text", [TODO_STATUS.IN_PROGRESS]: "accent", [TODO_STATUS.DONE]: "dim" };
92
- const GLYPH_ROLE: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "muted", [TODO_STATUS.IN_PROGRESS]: "accent", [TODO_STATUS.DONE]: "success" };
93
+ const STATUS_ALIASES: Record<string, TodoStatus> = {
94
+ completed: TODO_STATUS.DONE,
95
+ complete: TODO_STATUS.DONE,
96
+ doing: TODO_STATUS.IN_PROGRESS,
97
+ todo: TODO_STATUS.PENDING,
98
+ cancelled: TODO_STATUS.DROPPED,
99
+ canceled: TODO_STATUS.DROPPED,
100
+ abandoned: TODO_STATUS.DROPPED,
101
+ };
102
+ const STATUS_LIST = "pending, in_progress, blocked, done, or dropped";
103
+ const BLOCKED_NEEDS_NOTE = "blocked needs a note naming what it waits for";
104
+ const STATUS_GLYPH: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "○", [TODO_STATUS.IN_PROGRESS]: "◐", [TODO_STATUS.BLOCKED]: "⊘", [TODO_STATUS.DONE]: "✓", [TODO_STATUS.DROPPED]: "✕" };
105
+ const STATUS_ROLE: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "text", [TODO_STATUS.IN_PROGRESS]: "accent", [TODO_STATUS.BLOCKED]: "text", [TODO_STATUS.DONE]: "dim", [TODO_STATUS.DROPPED]: "dim" };
106
+ const GLYPH_ROLE: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "muted", [TODO_STATUS.IN_PROGRESS]: "accent", [TODO_STATUS.BLOCKED]: "warning", [TODO_STATUS.DONE]: "success", [TODO_STATUS.DROPPED]: "muted" };
93
107
  const NOTE_ROLE = "muted";
94
108
  const STALE_AFTER_TURNS = 2;
95
109
  /** Above this many rows the finished tasks fold into one line and the rest is capped. */
@@ -111,20 +125,30 @@ function parseStatus(value: unknown): TodoStatus | undefined {
111
125
  return STATUS_ALIASES[normalized];
112
126
  }
113
127
 
128
+ function countStatus(state: TodoState, status: TodoStatus): number {
129
+ return state.tasks.filter((task) => task.status === status).length;
130
+ }
131
+
132
+ /** Dropped tasks are neither done nor open. */
114
133
  export function todoSummary(state: TodoState): TodoSummary {
115
- const done = state.tasks.filter((task) => task.status === TODO_STATUS.DONE).length;
116
- return { done, total: state.tasks.length, open: state.tasks.length - done };
134
+ const done = countStatus(state, TODO_STATUS.DONE);
135
+ return { done, total: state.tasks.length, open: state.tasks.length - done - countStatus(state, TODO_STATUS.DROPPED) };
117
136
  }
118
137
 
138
+ // Only work the model can pick up goes stale: a list whose open tasks all
139
+ // wait on something outside it has nothing to bring up to date.
119
140
  export function staleTurns(state: TodoState, currentTurn: number): number {
120
- if (state.updatedTurn === null || todoSummary(state).open === 0) return 0;
141
+ const actionable = state.tasks.some((task) => task.status === TODO_STATUS.PENDING || task.status === TODO_STATUS.IN_PROGRESS);
142
+ if (state.updatedTurn === null || !actionable) return 0;
121
143
  return Math.max(0, currentTurn - state.updatedTurn);
122
144
  }
123
145
 
124
146
  function summaryText(state: TodoState): string {
125
147
  const { done, total } = todoSummary(state);
126
- const inProgress = state.tasks.filter((task) => task.status === TODO_STATUS.IN_PROGRESS).length;
127
- return `${total} ${total === 1 ? "task" : "tasks"} · ${done} done · ${inProgress} in progress`;
148
+ const inProgress = countStatus(state, TODO_STATUS.IN_PROGRESS);
149
+ const blocked = countStatus(state, TODO_STATUS.BLOCKED);
150
+ const dropped = countStatus(state, TODO_STATUS.DROPPED);
151
+ return `${total} ${total === 1 ? "task" : "tasks"} · ${done} done · ${inProgress} in progress${blocked ? ` · ${blocked} blocked` : ""}${dropped ? ` · ${dropped} dropped` : ""}`;
128
152
  }
129
153
 
130
154
  function taskLine(task: TodoTask): string {
@@ -135,8 +159,9 @@ function buildTask(input: TodoTaskInput, id: number): TodoTask | string {
135
159
  const title = cleanText(input.title);
136
160
  if (title.length === 0) return "title is required";
137
161
  const status = parseStatus(input.status);
138
- if (status === undefined) return `unknown status "${input.status}" (use pending, in_progress, or done)`;
162
+ if (status === undefined) return `unknown status "${input.status}" (use ${STATUS_LIST})`;
139
163
  const note = cleanText(input.note);
164
+ if (status === TODO_STATUS.BLOCKED && note.length === 0) return BLOCKED_NEEDS_NOTE;
140
165
  return { id, title, status, ...(note ? { note } : {}) };
141
166
  }
142
167
 
@@ -240,6 +265,7 @@ export function todoPromptBlock(state: TodoState, stale: number): string | undef
240
265
  return [
241
266
  "## Todo list",
242
267
  "Keep it current with the `todo` tool: mark a task in_progress before starting it, done right after finishing it, and rewrite the whole list with `write` whenever the plan changes. Update it before you end the turn.",
268
+ "A blocked task waits on something outside the list: do not work on it until its condition holds. When the plan changes, mark the tasks it made obsolete dropped instead of leaving them pending or marking them done.",
243
269
  ...lines,
244
270
  ].join("\n") + staleLine;
245
271
  }
@@ -253,18 +279,22 @@ function taskRow(task: TodoTask, theme: TodoTheme, inner: number): string {
253
279
  .join("\n");
254
280
  }
255
281
  const title = task.title;
256
- const note = task.status === TODO_STATUS.IN_PROGRESS && task.note ? ` ${theme.fg(NOTE_ROLE, "·")} ${theme.fg(NOTE_ROLE, task.note)}` : "";
282
+ const showsNote = task.status === TODO_STATUS.IN_PROGRESS || task.status === TODO_STATUS.BLOCKED;
283
+ const note = showsNote && task.note ? ` ${theme.fg(NOTE_ROLE, "·")} ${theme.fg(NOTE_ROLE, task.note)}` : "";
257
284
  return `${theme.fg(GLYPH_ROLE[task.status], STATUS_GLYPH[task.status])} ${theme.fg(STATUS_ROLE[task.status], title)}${note}`;
258
285
  }
259
286
 
260
- // Long lists keep the card short: done tasks fold into "✓ N done" and the
261
- // open ones fill the remaining rows, with a trailing count for the rest.
287
+ // Long lists keep the card short: done and dropped tasks fold into "✓ N done"
288
+ // and "✕ N dropped", and the open ones fill the remaining rows, with a
289
+ // trailing count for the rest.
262
290
  function bodyRows(state: TodoState, theme: TodoTheme, inner: number): string[] {
263
291
  if (state.tasks.length <= ROW_CAP) return state.tasks.map((task) => taskRow(task, theme, inner));
264
- const { done } = todoSummary(state);
265
- const open = state.tasks.filter((task) => task.status !== TODO_STATUS.DONE);
292
+ const open = state.tasks.filter((task) => task.status !== TODO_STATUS.DONE && task.status !== TODO_STATUS.DROPPED);
266
293
  const rows: string[] = [];
267
- if (done > 0) rows.push(`${theme.fg(GLYPH_ROLE[TODO_STATUS.DONE], STATUS_GLYPH[TODO_STATUS.DONE])} ${theme.fg(NOTE_ROLE, `${done} done`)}`);
294
+ for (const [status, label] of [[TODO_STATUS.DONE, "done"], [TODO_STATUS.DROPPED, "dropped"]] as const) {
295
+ const count = countStatus(state, status);
296
+ if (count > 0) rows.push(`${theme.fg(GLYPH_ROLE[status], STATUS_GLYPH[status])} ${theme.fg(NOTE_ROLE, `${count} ${label}`)}`);
297
+ }
268
298
  const room = ROW_CAP - rows.length - (open.length > ROW_CAP - rows.length ? 1 : 0);
269
299
  for (const task of open.slice(0, room)) rows.push(taskRow(task, theme, inner));
270
300
  if (open.length > room) rows.push(theme.fg(NOTE_ROLE, `… ${open.length - room} more`));
@@ -276,6 +306,8 @@ function collapsedRow(state: TodoState, theme: TodoTheme, inner: number): string
276
306
  if (active) return taskRow(active, theme, inner);
277
307
  const pending = state.tasks.find((task) => task.status === TODO_STATUS.PENDING);
278
308
  if (pending) return taskRow(pending, theme, inner);
309
+ const blocked = state.tasks.find((task) => task.status === TODO_STATUS.BLOCKED);
310
+ if (blocked) return taskRow(blocked, theme, inner);
279
311
  const { open } = todoSummary(state);
280
312
  return `${theme.fg(GLYPH_ROLE[TODO_STATUS.PENDING], STATUS_GLYPH[TODO_STATUS.PENDING])} ${theme.fg(NOTE_ROLE, `${open} open`)}`;
281
313
  }
@@ -288,6 +320,8 @@ export function todoCardTone(staleTurns: number): CardTone {
288
320
  export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number, options: TodoRenderOptions): string[] {
289
321
  if (state.tasks.length === 0) return [];
290
322
  const { done, total } = todoSummary(state);
323
+ // Dropped work is not part of the plan anymore, so it leaves the denominator.
324
+ const planned = total - countStatus(state, TODO_STATUS.DROPPED);
291
325
  const stale = options.staleTurns >= STALE_AFTER_TURNS;
292
326
  const action = options.collapsed ? "expand" : "collapse";
293
327
  const actionLabel = action[0]!.toUpperCase() + action.slice(1);
@@ -300,12 +334,21 @@ export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number
300
334
  // Content columns of the card body in the active style (float is narrower).
301
335
  const inner = panelInnerWidth(theme, width, tone);
302
336
  const rows = options.collapsed ? [collapsedRow(state, theme, inner)] : options.scrollable ? state.tasks.map((task) => taskRow(task, theme, inner)) : bodyRows(state, theme, inner);
303
- const body = stale ? [theme.fg(NOTE_ROLE, `stale · ${options.staleTurns} turns`), ...rows] : rows;
337
+ const staleLabel = theme.fg(NOTE_ROLE, `stale · ${options.staleTurns} turns`);
338
+ let body = options.collapsed
339
+ ? [truncateToWidth(`${stale ? `${staleLabel} · ` : ""}${rows[0]}`, inner, "…")]
340
+ : stale ? [staleLabel, ...rows] : rows;
341
+ if (!options.collapsed && !options.scrollable) {
342
+ // Budget physical lines after wrapping: one long task can exceed the
343
+ // old task-count cap by itself. Scrollable hosts retain every line.
344
+ body = body.flatMap((row) => wrapTextWithAnsi(row, Math.max(1, inner)));
345
+ if (body.length > ROW_CAP) body = [...body.slice(0, ROW_CAP - 1), theme.fg(NOTE_ROLE, "… more")];
346
+ }
304
347
  // The clickable control paints the shared hover role while hovered --
305
348
  // same treatment every other clickable surface uses -- instead of its
306
349
  // ordinary accent role.
307
350
  return renderCard(
308
- { title: `Todos ${paintHoverable(theme, control, options.hovered, "accent")}`, subtitle: `${done} of ${total}`, body, tone, glyph: TODO_GLYPH },
351
+ { title: `Todos ${paintHoverable(theme, control, options.hovered, "accent")}`, subtitle: `${done} of ${planned}`, body, tone, glyph: TODO_GLYPH },
309
352
  theme,
310
353
  width,
311
354
  { expanded: true, hint, panel: true },
@@ -269,7 +269,7 @@ export function formatReset(resetAt: number | null, now: number): string {
269
269
  }
270
270
 
271
271
  function parseWindow(raw: RawWindow | null | undefined, now: number): UsageWindow | undefined {
272
- if (!raw || typeof raw.used_percent !== "number" || typeof raw.limit_window_seconds !== "number") return undefined;
272
+ if (!raw || !isFiniteNumber(raw.used_percent) || !isFiniteNumber(raw.limit_window_seconds) || raw.limit_window_seconds <= 0) return undefined;
273
273
  const resetAt =
274
274
  typeof raw.reset_at === "number" ? raw.reset_at * 1000 : typeof raw.reset_after_seconds === "number" ? now + raw.reset_after_seconds * 1000 : null;
275
275
  return { label: windowLabel(raw.limit_window_seconds), usedPercent: raw.used_percent, windowSeconds: raw.limit_window_seconds, resetAt };
@@ -297,9 +297,10 @@ export function parseCodexUsage(payload: unknown, now: number): ProviderUsage {
297
297
  function headerWindow(headers: Record<string, string>, kind: "primary" | "secondary"): UsageWindow | undefined {
298
298
  const used = Number.parseFloat(headers[`${HEADER_PREFIX}${kind}-used-percent`] ?? "");
299
299
  if (!Number.isFinite(used)) return undefined;
300
- const minutes = Number.parseInt(headers[`${HEADER_PREFIX}${kind}-window-minutes`] ?? "", 10);
300
+ // Missing or invalid duration is not a zero-minute quota or an assumed 5h window.
301
+ const seconds = Number(headers[`${HEADER_PREFIX}${kind}-window-minutes`]) * MINUTE;
302
+ if (!Number.isFinite(seconds) || seconds <= 0) return undefined;
301
303
  const resetAt = Number.parseInt(headers[`${HEADER_PREFIX}${kind}-reset-at`] ?? "", 10);
302
- const seconds = Number.isFinite(minutes) ? minutes * MINUTE : 0;
303
304
  return { label: windowLabel(seconds), usedPercent: used, windowSeconds: seconds, resetAt: Number.isFinite(resetAt) ? resetAt * 1000 : null };
304
305
  }
305
306
 
@@ -0,0 +1,201 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
+ import type { TSchema } from "typebox";
4
+
5
+ const MAX_STATUSES = 8;
6
+ const MAX_ACTIVE_OBSERVATIONS = 8;
7
+ type Outcome = { kind: "success" | "cli_error" | "native_failure" | "unknown"; code?: string; phase?: string; mutation_outcome?: string; retry_safe?: boolean; next_action?: string };
8
+ interface StatusSample {
9
+ timeout_ms: number;
10
+ total_ms: number;
11
+ resolution_ms: number;
12
+ adapter_ms: number;
13
+ decode_ms: number;
14
+ outcome: Outcome;
15
+ }
16
+ interface Summary {
17
+ schema: "gentle-pi.status-timing/v1";
18
+ host_total_ms: number;
19
+ dispatch_ms: number;
20
+ sidebar_ms: number;
21
+ completion_ms: number;
22
+ truncated: boolean;
23
+ clock_unavailable?: true;
24
+ statuses: StatusSample[];
25
+ }
26
+ interface HostObservation {
27
+ clock: () => number;
28
+ current: () => boolean;
29
+ claim: () => boolean;
30
+ claimed: boolean;
31
+ started: number;
32
+ lastNativeEnd: number;
33
+ summary: Summary;
34
+ }
35
+ interface NativeObservation { host: HostObservation; sample: StatusSample; }
36
+ const hostScope = new AsyncLocalStorage<HostObservation>();
37
+ const nativeScope = new AsyncLocalStorage<NativeObservation>();
38
+
39
+ // Observer failures must never replace the operation's result or exception.
40
+ function safely<T>(run: () => T): T | undefined {
41
+ try { return run(); } catch { return undefined; }
42
+ }
43
+ function now(host: HostObservation): number {
44
+ const value = safely(host.clock);
45
+ if (value !== undefined && Number.isFinite(value) && value >= 0) return value;
46
+ host.summary.clock_unavailable = true;
47
+ return 0;
48
+ }
49
+ function elapsed(start: number, end: number): number { return Math.max(0, end - start); }
50
+
51
+ // Failure/v2 codes and next_action are extensible strings, not safe diagnostic
52
+ // text. Only recognized values enter this summary; no messages or raw envelopes.
53
+ const CLI_CODES = new Set(["unavailable", "timeout", "non-zero", "signal", "unexpected-stderr", "output-limit", "empty-output", "malformed-json", "schema-incompatible", "identity-mismatch", "version-incompatible", "cancelled", "package-local-binary-missing", "unsupported-transition-operation"]);
54
+ const NATIVE_CODES = new Set(["operation_timeout", "invalid_request", "binding_revision_conflict", "internal_error", "cancelled"]);
55
+ function outcome(error: unknown): Outcome {
56
+ return safely((): Outcome => {
57
+ if (typeof error !== "object" || error === null) return { kind: "unknown" };
58
+ const value = error as { name?: unknown; code?: unknown; failureEnvelope?: Record<string, unknown> };
59
+ if (value.name === "NativeReviewCliError") return { kind: "cli_error", code: typeof value.code === "string" && CLI_CODES.has(value.code) ? value.code : "other" };
60
+ if (value.name !== "NativeReviewIntegrationError" || !value.failureEnvelope) return { kind: "unknown" };
61
+ const failure = value.failureEnvelope;
62
+ const result: Outcome = { kind: "native_failure", code: typeof failure.code === "string" && NATIVE_CODES.has(failure.code) ? failure.code : "other" };
63
+ if (["preflight", "pre_native", "native_running", "native_committed", "reconciliation"].includes(String(failure.phase))) result.phase = String(failure.phase);
64
+ if (["not_started", "committed", "unknown"].includes(String(failure.mutationOutcome))) result.mutation_outcome = String(failure.mutationOutcome);
65
+ if (typeof failure.retrySafe === "boolean") result.retry_safe = failure.retrySafe;
66
+ if (["stop", "review.status"].includes(String(failure.nextAction))) result.next_action = String(failure.nextAction);
67
+ return result;
68
+ }) ?? { kind: "unknown" };
69
+ }
70
+
71
+ /** The next already-authorized STATUS-bearing tool call consumes the arm. */
72
+ export class StatusTimingDiagnostics {
73
+ private armed = false;
74
+ private epoch = 0;
75
+ private manager: ExtensionContext["sessionManager"] | undefined;
76
+ private session: string | undefined;
77
+ private last: Summary | undefined;
78
+ private readonly active = new Set<HostObservation>();
79
+ private readonly clock: () => number;
80
+ constructor(clock = () => performance.now()) { this.clock = clock; }
81
+
82
+ reset(ctx?: ExtensionContext): void {
83
+ this.epoch++;
84
+ this.armed = false;
85
+ this.last = undefined;
86
+ for (const host of this.active) host.summary.statuses.length = 0;
87
+ this.active.clear();
88
+ this.manager = ctx?.sessionManager;
89
+ this.session = safely(() => ctx?.sessionManager.getSessionId());
90
+ }
91
+ private matches(ctx: ExtensionContext): boolean {
92
+ return safely(() => this.manager === ctx.sessionManager && !!this.session && this.session === ctx.sessionManager.getSessionId()) === true;
93
+ }
94
+ command(args: string, ctx: ExtensionContext): void {
95
+ if (!this.matches(ctx)) this.reset(ctx);
96
+ const action = args.trim() || "show";
97
+ if (action === "enable") {
98
+ this.reset(ctx);
99
+ this.armed = !!this.session;
100
+ ctx.ui.notify(this.armed
101
+ ? "STATUS timing armed for the next separately authorized STATUS-bearing tool call. No STATUS is invoked or retried. Use /gentle:status-timing show to consult the completed summary."
102
+ : "STATUS timing requires a live session.", "info");
103
+ } else if (action === "disable") {
104
+ this.reset(ctx);
105
+ ctx.ui.notify("STATUS timing disabled; in-memory observations cleared.", "info");
106
+ } else if (action === "show") {
107
+ ctx.ui.notify(this.last ? JSON.stringify(this.last, null, 2) : `No completed STATUS timing summary. Diagnostic is ${this.armed ? "armed" : "off"}.`, "info");
108
+ } else {
109
+ ctx.ui.notify("Unknown /gentle:status-timing sub-action. Use enable, disable, or show.", "warning");
110
+ }
111
+ }
112
+ tool<TParams extends TSchema, TDetails>(definition: ToolDefinition<TParams, TDetails>): ToolDefinition<TParams, TDetails> {
113
+ return {
114
+ ...definition,
115
+ execute: async (toolCallId, params, signal, onUpdate, ctx) => {
116
+ const run = () => definition.execute(toolCallId, params, signal, onUpdate, ctx);
117
+ if (!this.matches(ctx)) this.reset(ctx);
118
+ if (!this.armed || this.active.size >= MAX_ACTIVE_OBSERVATIONS) return run();
119
+ const epoch = this.epoch;
120
+ const host: HostObservation = {
121
+ clock: this.clock,
122
+ current: () => this.epoch === epoch && this.matches(ctx),
123
+ claim: () => {
124
+ if (!host.current() || !this.armed) return false;
125
+ this.armed = false;
126
+ return true;
127
+ },
128
+ claimed: false, started: 0, lastNativeEnd: 0,
129
+ summary: { schema: "gentle-pi.status-timing/v1", host_total_ms: 0, dispatch_ms: 0, sidebar_ms: 0, completion_ms: 0, truncated: false, statuses: [] },
130
+ };
131
+ this.active.add(host);
132
+ host.started = now(host);
133
+ return hostScope.run(host, async () => {
134
+ try { return await run(); }
135
+ finally {
136
+ this.active.delete(host);
137
+ safely(() => {
138
+ if (!host.claimed || !host.current()) return;
139
+ const ended = now(host);
140
+ host.summary.host_total_ms = elapsed(host.started, ended);
141
+ host.summary.completion_ms = elapsed(host.lastNativeEnd, ended);
142
+ this.last = host.summary;
143
+ });
144
+ }
145
+ });
146
+ },
147
+ };
148
+ }
149
+ }
150
+
151
+ /** Only called by the existing STATUS entry points; never invokes one itself. */
152
+ export async function observeNativeStatus<T>(timeoutMs: number, run: () => Promise<T>): Promise<T> {
153
+ const host = hostScope.getStore();
154
+ if (!host || !safely(host.current)) return run();
155
+ if (!host.claimed) {
156
+ if (!safely(host.claim)) return run();
157
+ host.claimed = true;
158
+ host.summary.dispatch_ms = elapsed(host.started, now(host));
159
+ }
160
+ const sample: StatusSample = { timeout_ms: timeoutMs, total_ms: 0, resolution_ms: 0, adapter_ms: 0, decode_ms: 0, outcome: { kind: "success" } };
161
+ if (host.summary.statuses.length < MAX_STATUSES) host.summary.statuses.push(sample);
162
+ else host.summary.truncated = true;
163
+ const started = now(host);
164
+ return nativeScope.run({ host, sample }, async () => {
165
+ try { return await run(); }
166
+ catch (error) { safely(() => { sample.outcome = outcome(error); }); throw error; }
167
+ finally {
168
+ safely(() => {
169
+ const ended = now(host);
170
+ sample.total_ms = elapsed(started, ended);
171
+ host.lastNativeEnd = ended;
172
+ });
173
+ }
174
+ });
175
+ }
176
+
177
+ function activeNative(): NativeObservation | undefined {
178
+ const observation = nativeScope.getStore();
179
+ return observation && safely(observation.host.current) ? observation : undefined;
180
+ }
181
+ export function measureStatusSync<T>(stage: "resolution" | "decode", run: () => T): T {
182
+ const observation = activeNative();
183
+ if (!observation) return run();
184
+ const started = now(observation.host);
185
+ try { return run(); }
186
+ finally { safely(() => { observation.sample[`${stage}_ms`] += elapsed(started, now(observation.host)); }); }
187
+ }
188
+ export async function measureStatusAsync<T>(stage: "adapter", run: () => Promise<T>): Promise<T> {
189
+ const observation = activeNative();
190
+ if (!observation) return run();
191
+ const started = now(observation.host);
192
+ try { return await run(); }
193
+ finally { safely(() => { observation.sample[`${stage}_ms`] += elapsed(started, now(observation.host)); }); }
194
+ }
195
+ export function measureStatusSidebar<T>(run: () => T): T {
196
+ const host = hostScope.getStore();
197
+ if (!host || !safely(host.current)) return run();
198
+ const started = now(host);
199
+ try { return run(); }
200
+ finally { safely(() => { host.summary.sidebar_ms += elapsed(started, now(host)); }); }
201
+ }
@@ -1,4 +1,5 @@
1
1
  import { spawn as nodeSpawn } from "node:child_process";
2
+ import { gentleAiIsolationEnv } from "./gentle-ai-binary.ts";
2
3
 
3
4
  // gentle-ai#4309 owns anonymous usage telemetry end to end: install and
4
5
  // heartbeat events, the notice, every kill switch, and the wire contract.
@@ -128,7 +129,8 @@ export function spawnTelemetryTrigger(options: SpawnTelemetryTriggerOptions): Te
128
129
  try {
129
130
  const child = spawnImpl(options.executable, ["telemetry", "trigger", "--json"], {
130
131
  cwd: options.cwd,
131
- env: options.env,
132
+ // S9: a bundled install keeps Gentle AI's telemetry state inside its prefix.
133
+ env: gentleAiIsolationEnv(options.env),
132
134
  detached: true,
133
135
  windowsHide: true,
134
136
  stdio: "ignore",
@@ -1,12 +1,16 @@
1
1
  import { CURSOR_MARKER, Editor, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import { createRequire } from "node:module";
3
3
 
4
- // Private shape audited against @earendil-works/pi-tui 0.99.1, 0.99.2 and 1.0.0:
4
+ // Private shape audited against @earendil-works/pi-tui 0.99.1, 0.99.2, 1.0.0 and 1.1.0:
5
5
  // editor.js state/pastes/history, sticky cursor reset, layoutText/render geometry,
6
6
  // autocomplete cancellation and { state, pastes, pasteCounter } undo restoration;
7
7
  // undo-stack.js clone-on-push/pop snapshots. The two 0.99 releases ship identical
8
8
  // editor/undo-stack sources; 1.0.0 is a touched-contract audit, not byte identity.
9
- // Actual 1.0.0 bundled and unbundled tests exercise identity, edit/undo, paste,
9
+ // The 1.1.0 touched-contract audit retains clone-on-push snapshots with the
10
+ // paste registry, cursor resets, layoutText/render geometry and history exit.
11
+ // cancelAutocomplete aborts/debounces requests before clearing its UI;
12
+ // the adapter still calls that host-owned method, never mutating its request state.
13
+ // Actual installed bundled/unbundled tests exercise identity, edit/undo, paste,
10
14
  // selection, wrapping/scroll and autocomplete; fabricated metadata tests only
11
15
  // prove exact-version admission for the older audited releases.
12
16
  // Never silently adapt another build: undo also owns the paste registry.
@@ -47,7 +51,7 @@ interface PrivateEditor {
47
51
 
48
52
  // The single audited-release source for the adapter and Gentle Shell's runtime
49
53
  // identity gates. Add a release only after re-auditing the files named above.
50
- export const AUDITED_PI_EDITOR_VERSIONS = Object.freeze(["0.99.1", "0.99.2", "1.0.0"] as const);
54
+ export const AUDITED_PI_EDITOR_VERSIONS = Object.freeze(["0.99.1", "0.99.2", "1.0.0", "1.1.0"] as const);
51
55
  export type AuditedPiEditorVersion = (typeof AUDITED_PI_EDITOR_VERSIONS)[number];
52
56
 
53
57
  export function isAuditedPiEditorVersion(version: unknown): version is AuditedPiEditorVersion {
@@ -18,6 +18,8 @@ export interface VisualVisibility {
18
18
  todo: boolean;
19
19
  usageCost: boolean;
20
20
  modelDetails: boolean;
21
+ /** High-context warning notice; independent from audio notifications. */
22
+ contextWarning: boolean;
21
23
  }
22
24
  export interface VisualSettings {
23
25
  statusPlacement: StatusPlacement;
@@ -29,13 +31,13 @@ export const DEFAULT_VISUAL_SETTINGS: VisualSettings = {
29
31
  statusPlacement: STATUS_PLACEMENT.AUTO,
30
32
  headerPlacement: HEADER_PLACEMENT.TOP,
31
33
  density: DENSITY.COMFORTABLE,
32
- visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true },
34
+ visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true },
33
35
  };
34
36
  /** Sections row order; `rdd` follows `changes` as the RDD group follows Changes in the Status card. */
35
- export const VISUAL_SECTION_KEYS = ["changes", "rdd", "agents", "todo", "usageCost", "modelDetails"] as const satisfies readonly (keyof VisualVisibility)[];
36
- /** Keys added after v1 shipped: stored settings may omit them together, and they default to shown. */
37
- const ADDED_SECTION_KEYS: readonly (keyof VisualVisibility)[] = ["rdd"];
38
- const LEGACY_SECTION_KEYS = VISUAL_SECTION_KEYS.filter((key) => !ADDED_SECTION_KEYS.includes(key));
37
+ export const VISUAL_SECTION_KEYS = ["changes", "rdd", "agents", "todo", "usageCost", "modelDetails", "contextWarning"] as const satisfies readonly (keyof VisualVisibility)[];
38
+ /** Keys added after v1 shipped: stored settings may omit them together or one at a time, and they default to shown. */
39
+ const ADDED_SECTION_KEYS: readonly (keyof VisualVisibility)[] = ["rdd", "contextWarning"];
40
+ const BASE_SECTION_KEYS = VISUAL_SECTION_KEYS.filter((key) => !ADDED_SECTION_KEYS.includes(key));
39
41
  interface VisualOptions { gentlePiConfigHome?: string }
40
42
  export interface VisualResolution {
41
43
  settings: VisualSettings;
@@ -64,13 +66,22 @@ export function isVisualSettings(value: unknown): value is VisualSettings {
64
66
  && keysMatch(visibility, VISUAL_SECTION_KEYS)
65
67
  && Object.values(visibility).every((item) => typeof item === "boolean");
66
68
  }
67
- /** Reader-side validation: accepts the current shape and legacy visibility that predates added sections. Writers stay strict. */
69
+ /**
70
+ * Reader-side validation: accepts the current shape plus every historical one.
71
+ * Every key must be known, every base key present, and every present value a
72
+ * boolean; missing added keys (`rdd`, `contextWarning`) then default to shown,
73
+ * while present values (including `rdd: false`) are preserved. Writers stay strict.
74
+ */
68
75
  export function normalizeVisualSettings(value: unknown): VisualSettings | undefined {
69
76
  if (!record(value) || !record(value.visibility)) return undefined;
70
77
  const visibility = value.visibility;
71
- const candidate = keysMatch(visibility, LEGACY_SECTION_KEYS)
72
- ? { ...value, visibility: Object.fromEntries(VISUAL_SECTION_KEYS.map((key) => [key, visibility[key] ?? true])) }
73
- : value;
78
+ if (!Object.keys(visibility).every((key) => (VISUAL_SECTION_KEYS as readonly string[]).includes(key))) return undefined;
79
+ if (!BASE_SECTION_KEYS.every((key) => Object.hasOwn(visibility, key))) return undefined;
80
+ // A key that is present is authoritative: only a missing added key may default to shown.
81
+ if (!Object.keys(visibility).every((key) => typeof visibility[key] === "boolean")) return undefined;
82
+ const candidate = keysMatch(visibility, VISUAL_SECTION_KEYS)
83
+ ? value
84
+ : { ...value, visibility: Object.fromEntries(VISUAL_SECTION_KEYS.map((key) => [key, visibility[key] ?? true])) };
74
85
  return isVisualSettings(candidate) ? candidate : undefined;
75
86
  }
76
87
  export function parseVisualSettingsFile(raw: string): VisualSettings | undefined {