gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,129 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { initTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { buildShellBarModel, type ShellFooterData } from "../extensions/gentle-shell.ts";
5
+ import { createQuietToolRenderer } from "../extensions/quiet-tools.ts";
6
+ import { CARD_STYLE, cardStyle, setCardStyle } from "../lib/shell-card.ts";
7
+
8
+ initTheme("dark");
9
+ const pi = { getThinkingLevel: () => "high" } as unknown as ExtensionAPI;
10
+ const footer = { getGitBranch: () => "main", getExtensionStatuses: () => new Map() } as unknown as ShellFooterData;
11
+
12
+ function sessionFixture() {
13
+ let reads = 0;
14
+ let projections = 0;
15
+ let sessionId = "session-1";
16
+ let leaf = "leaf-1";
17
+ const entries = [{ type: "message", message: { role: "assistant", usage: { cost: { total: 1 } } } }];
18
+ const ctx = {
19
+ model: { id: "model", contextWindow: 1000 },
20
+ getContextUsage() { projections++; return { percent: 10, contextWindow: this.model.contextWindow }; },
21
+ modelRegistry: { isUsingOAuth: () => false },
22
+ sessionManager: {
23
+ getEntries() { reads++; return [...entries]; },
24
+ getEntryCount: () => entries.length,
25
+ getSessionId: () => sessionId,
26
+ getLeafId: () => leaf,
27
+ getCwd: () => "/repo",
28
+ getSessionName: () => "session",
29
+ },
30
+ };
31
+ return {
32
+ ctx, entries,
33
+ build: () => buildShellBarModel(pi, ctx as unknown as ExtensionContext, footer),
34
+ counts: () => [reads, projections],
35
+ branch: () => { leaf = "other-leaf"; },
36
+ switchSession: () => { sessionId = "session-2"; },
37
+ };
38
+ }
39
+
40
+ test("settled shell chrome scans stats once while presentation stays live", () => {
41
+ const fixture = sessionFixture();
42
+ assert.equal(fixture.build().costTotal, 1);
43
+ for (let frame = 0; frame < 20; frame++) fixture.build();
44
+ assert.deepEqual(fixture.counts(), [1, 1]);
45
+ fixture.ctx.sessionManager.getSessionName = () => "renamed";
46
+ assert.equal(fixture.build().sessionName, "renamed");
47
+ assert.deepEqual(fixture.counts(), [1, 1]);
48
+ fixture.entries.push(fixture.entries[0]!);
49
+ assert.equal(fixture.build().costTotal, 2);
50
+ fixture.branch(); fixture.build();
51
+ fixture.switchSession(); fixture.build();
52
+ fixture.ctx.model = { ...fixture.ctx.model, id: "new-model" }; fixture.build();
53
+ fixture.ctx.model.contextWindow = 2000;
54
+ assert.equal(fixture.build().contextWindow, 2000);
55
+ assert.deepEqual(fixture.counts(), [6, 6]);
56
+ const other = sessionFixture(); other.build();
57
+ assert.deepEqual(other.counts(), [1, 1], "manager identities do not share statistics");
58
+ });
59
+
60
+ test("leaf-based stats work on read-only hosts without an entry-count method", () => {
61
+ const fixture = sessionFixture();
62
+ Reflect.deleteProperty(fixture.ctx.sessionManager, "getEntryCount");
63
+ fixture.build(); fixture.build();
64
+ assert.deepEqual(fixture.counts(), [1, 1]);
65
+ fixture.branch(); fixture.build();
66
+ assert.deepEqual(fixture.counts(), [2, 2]);
67
+ });
68
+
69
+ function countingTheme() {
70
+ let calls = 0;
71
+ let prefix = "";
72
+ return {
73
+ theme: {
74
+ fg: (_color: string, text: string) => { calls++; return prefix + text; },
75
+ bg: (_color: string, text: string) => { calls++; return `\x1b[48;5;235m${text}\x1b[49m`; },
76
+ bold: (text: string) => text,
77
+ },
78
+ calls: () => calls,
79
+ change: () => { prefix += "!"; },
80
+ };
81
+ }
82
+
83
+ for (const expanded of [false, true]) test(`settled tool cards cache complete rows (expanded=${expanded})`, () => {
84
+ const previous = cardStyle();
85
+ try {
86
+ setCardStyle(CARD_STYLE.NEON);
87
+ const { theme, calls, change } = countingTheme();
88
+ const renderer = createQuietToolRenderer("read");
89
+ const context = { args: { path: "src/example.ts" }, state: {}, isPartial: false, executionStarted: true };
90
+ const result = { content: [{ type: "text" as const, text: "first line\nsecond line" }], details: {} };
91
+ const cards = [renderer.renderCall!(context.args, theme as never, context as never),
92
+ renderer.renderResult!(result, { expanded, isPartial: false }, theme as never, context as never)];
93
+ for (const card of cards) {
94
+ const rows = card.render(80);
95
+ const warm = calls();
96
+ for (let frame = 0; frame < 20; frame++) assert.deepEqual(card.render(80), rows);
97
+ assert.equal(calls(), warm, "warm frames do not rebuild decorated lines");
98
+ assert.notDeepEqual(card.render(40), rows, "width invalidates layout");
99
+ setCardStyle(CARD_STYLE.FLOAT);
100
+ assert.notDeepEqual(card.render(80), rows, "style changes apply without host invalidation");
101
+ setCardStyle(CARD_STYLE.NEON);
102
+ card.render(80);
103
+ change(); card.invalidate();
104
+ assert.notDeepEqual(card.render(80), rows, "theme invalidation rebuilds colored content");
105
+ }
106
+ } finally { setCardStyle(previous); }
107
+ });
108
+
109
+ test("cached call chrome observes a result mark on its shared row state", () => {
110
+ const { theme } = countingTheme();
111
+ const renderer = createQuietToolRenderer("read");
112
+ const context = { args: { path: "file.ts" }, state: {}, isPartial: true, executionStarted: true };
113
+ const call = renderer.renderCall!(context.args, theme as never, context as never);
114
+ const running = call.render(80);
115
+ assert.ok(running.join("\n").includes("running"));
116
+ renderer.renderResult!({ content: [], details: {} }, { expanded: false, isPartial: false }, theme as never, context as never);
117
+ assert.ok(!call.render(80).join("\n").includes("running"), "result arrival removes the call's running/closing rows");
118
+ });
119
+
120
+ test("image renderers keep their live geometry instead of caching opaque components", () => {
121
+ const { theme } = countingTheme();
122
+ let renders = 0;
123
+ const renderer = createQuietToolRenderer("read", undefined, undefined, () => ({
124
+ render: () => [`image geometry ${++renders}`], invalidate() {},
125
+ }));
126
+ const card = renderer.renderResult!({ content: [{ type: "image", data: "", mimeType: "image/png" }], details: {} },
127
+ { expanded: true, isPartial: false }, theme as never, {} as never);
128
+ assert.notDeepEqual(card.render(80), card.render(80));
129
+ });
@@ -49,7 +49,7 @@ test("grouped Status preserves structured fields and opaque integration text", (
49
49
  const lines = renderShellSidebarBar({
50
50
  cwd: "/project", branch: "main", dirty: 2, sessionName: "session",
51
51
  modelId: "model", effort: "high", contextPercent: 45, contextWindow: 1000,
52
- costTotal: 1, subscription: false, statuses: ["opaque integration"],
52
+ costTotal: 1, subscription: false, usage: undefined, statuses: ["opaque integration"],
53
53
  }, painted, 46);
54
54
  const text = lines.join("\n");
55
55
  let previous = -1;
@@ -365,6 +365,31 @@ test("wheel scrolls the rail and is consumed at both boundaries and blank space"
365
365
  assert.equal(scroll.scrollTop, 0);
366
366
  });
367
367
 
368
+ test("wheel over a bounded Todo viewport scrolls the section before the surrounding rail", (t) => {
369
+ const f = fixture();
370
+ const received: TuiMouseEvent[] = [];
371
+ let consumes = true;
372
+ sidebarPart(f.tui, "todo", {
373
+ render: () => ["Todo viewport", "Todo body"], invalidate() {},
374
+ handleMouse(event: TuiMouseEvent) {
375
+ received.push(event);
376
+ return consumes && event.type === "wheel" ? { handled: true, render: true } : undefined;
377
+ },
378
+ });
379
+ t.after(installSidebar(f.tui, theme));
380
+ const scroll = rail(f);
381
+ const content = scroll.render(50);
382
+ scroll.updateLayout(content.length + 20, content.length, () => {});
383
+ const y = content.findIndex((line) => line.includes("Todo body"));
384
+ const event: TuiMouseEvent = { type: "wheel", button: "none", wheelDelta: 2, x: 3, y, screenX: 93, screenY: y, width: 50, height: content.length, shift: false, alt: false, ctrl: false };
385
+ assert.equal(scroll.handleMouse(event)?.handled, true);
386
+ assert.equal(scroll.scrollTop, 0, "inner viewport consumes wheel without moving the rail");
387
+ assert.equal(received.at(-1)?.y, 1, "section coordinates are local");
388
+ consumes = false;
389
+ assert.equal(scroll.handleMouse(event)?.handled, true);
390
+ assert.equal(scroll.scrollTop, 2, "unbounded/collapsed sections fall back to rail scrolling");
391
+ });
392
+
368
393
  test("rail dispatches a clipped, scroll-translated left click to only the matching sidebar part", (t) => {
369
394
  const f = fixture();
370
395
  let clicks = 0;
@@ -427,7 +452,7 @@ test("rail rejects removed or replaced parts before cached geometry is prepared
427
452
  return { handled: true };
428
453
  },
429
454
  };
430
- const mounted = sidebarPart(f.tui, "todo", { render: () => ["Todo bottom"], invalidate() {} }, original);
455
+ const mounted = sidebarPart(f.tui, "todo", { render: () => ["Todo bottom"], invalidate() {}, dispose() {} }, original);
431
456
  const dispose = installSidebar(f.tui, theme);
432
457
  t.after(dispose);
433
458
  const scroll = rail(f);
@@ -9,6 +9,7 @@ import {
9
9
  staleTurns,
10
10
  TODO_STATUS,
11
11
  todoPromptBlock,
12
+ todoStateMessage,
12
13
  todoSummary,
13
14
  type TodoState,
14
15
  } from "../lib/shell-todo.ts";
@@ -152,19 +153,49 @@ test("replayTodo rebuilds the latest state from Gentle and rpiv tool results ali
152
153
  assert.deepEqual(replayTodo([]), emptyTodo());
153
154
  });
154
155
 
155
- test("todoPromptBlock lists open work with the rules, and stays silent when there is nothing open", () => {
156
- const block = todoPromptBlock(seeded(), 0);
157
- assert.ok(block);
156
+ test("todoPromptBlock is the byte-stable rules block; todoStateMessage carries the open work (#1662)", () => {
157
+ const block = todoPromptBlock();
158
158
  assert.match(block, /^## Todo list/m);
159
159
  assert.match(block, /mark a task in_progress before starting/);
160
- assert.match(block, /1\. \[done\] Add quiet tool rendering/);
161
- assert.match(block, /2\. \[in_progress\] Fix quiet tools conflict — fixing conflict/);
162
- assert.match(block, /3\. \[pending\] Show git bash tails/);
163
160
  assert.doesNotMatch(block, /stale/);
164
- assert.match(todoPromptBlock(seeded(), 2) ?? "", /stale: 2 turns without an update/);
165
- assert.equal(todoPromptBlock(emptyTodo(), 0), undefined);
161
+ assert.doesNotMatch(block, /Add quiet tool rendering/, "the rules block never contains task content");
162
+ const message = todoStateMessage(seeded());
163
+ assert.ok(message);
164
+ assert.match(message, /1\. \[done\] Add quiet tool rendering/);
165
+ assert.match(message, /2\. \[in_progress\] Fix quiet tools conflict — fixing conflict/);
166
+ assert.match(message, /3\. \[pending\] Show git bash tails/);
167
+ assert.doesNotMatch(message, /stale/);
168
+ assert.equal(todoStateMessage(emptyTodo()), undefined);
166
169
  const allDone = applyTodo(seeded(), { action: "write", tasks: [{ title: "A", status: "done" }] }, 1).state;
167
- assert.equal(todoPromptBlock(allDone, 0), undefined);
170
+ assert.equal(todoStateMessage(allDone), undefined);
171
+ });
172
+
173
+ test("collapsed long tasks occupy one body line, including stale state, without losing task data", (t) => {
174
+ const title = "界🙂 long title ".repeat(100);
175
+ const note = "long description ".repeat(100);
176
+ for (const style of [CARD_STYLE.NEON, CARD_STYLE.FLOAT]) {
177
+ useCardStyle(t, style);
178
+ for (const status of ["in_progress", "pending", "blocked"]) {
179
+ const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title, note, status }] }, 1).state;
180
+ const snapshot = structuredClone(state);
181
+ for (const width of [16, 40, 70]) {
182
+ for (const staleTurns of [0, 4]) {
183
+ const rows = renderTodoCard(state, withBackground(plainTheme), width, { collapsed: true, staleTurns, scrollable: true });
184
+ assert.equal(rows.length, style === CARD_STYLE.FLOAT ? 5 : 3);
185
+ assert.ok(rows.some((row) => stripAnsi(row).includes("…")), "long text signals truncation");
186
+ for (const row of rows) assert.equal(visibleWidth(row), width);
187
+ }
188
+ }
189
+ assert.deepEqual(state, snapshot);
190
+ }
191
+ }
192
+ });
193
+
194
+ test("non-scrollable Todo caps physical wrapped rows, not task count", () => {
195
+ const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "long title ".repeat(300), status: "in_progress", note: "long note ".repeat(300) }] }, 1).state;
196
+ const rows = renderTodoCard(state, plainTheme, 40, { collapsed: false, staleTurns: 4 });
197
+ assert.ok(rows.length <= 14);
198
+ assert.match(stripAnsi(rows.at(-2)!), /…/);
168
199
  });
169
200
 
170
201
  test("renderTodoCard draws the framed list with status glyphs and keeps every line at width", () => {
@@ -209,8 +240,8 @@ test("renderTodoCard keeps the configured collapse shortcut in the header while
209
240
 
210
241
  const staleCollapsed = renderTodoCard(seeded(), plainTheme, 70, { collapsed: true, staleTurns: 2, collapseKey: "ctrl+shift+t" }).map(stripAnsi);
211
242
  assert.match(staleCollapsed[0], /^╭─ ❀ Todos ▸ Expand · 1 of 3 ─+ ctrl\+shift\+t expand ╮$/);
212
- assert.match(staleCollapsed[1], /^│ stale · 2 turns +│$/);
213
- assert.match(staleCollapsed[2], /^│ ◐ Fix quiet tools conflict · fixing conflict +│$/);
243
+ assert.equal(staleCollapsed.length, 3, "staleness stays in the single collapsed body row");
244
+ assert.match(staleCollapsed[1], /^│ stale · 2 turns · ◐ Fix quiet tools conflict · fixing conflict +│$/);
214
245
 
215
246
  const fallback = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "Finished first", status: "done" }, { title: "First pending" }, { title: "Later pending" }] }, 1).state;
216
247
  assert.match(renderTodoCard(fallback, plainTheme, 70, { collapsed: true, staleTurns: 0 }).map(stripAnsi)[1], /^│ ○ First pending +│$/);
@@ -379,3 +410,93 @@ test("a done todo wraps to the float body so the strikethrough never re-wraps",
379
410
  assertFloatRows(lines, 40);
380
411
  for (const row of lines.slice(3, -1)) assert.match(stripAnsi(row), /^ ▎ [✓ ] ~[^~]+~ +$/u, "each physical row closes its own strikethrough");
381
412
  });
413
+
414
+ // #1814 dropped: terminal, never counts as open, reads apart from done.
415
+ // #1820 blocked: open but not actionable, always carries its reason.
416
+
417
+ test("applyTodo accepts dropped and blocked, maps cancel vocabulary to dropped, and names every status on an unknown one", () => {
418
+ const { state, error } = applyTodo(
419
+ emptyTodo(),
420
+ { action: "write", tasks: [{ title: "Old plan", status: "dropped" }, { title: "Deploy", status: "blocked", note: "waiting for an admin" }, { title: "A", status: "cancelled" }, { title: "B", status: "canceled" }, { title: "C", status: "abandoned" }] },
421
+ 1,
422
+ );
423
+ assert.equal(error, undefined);
424
+ assert.deepEqual(state.tasks.map((task) => task.status), [TODO_STATUS.DROPPED, TODO_STATUS.BLOCKED, TODO_STATUS.DROPPED, TODO_STATUS.DROPPED, TODO_STATUS.DROPPED]);
425
+ assert.equal(state.tasks[1].note, "waiting for an admin");
426
+ const bad = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "X", status: "later" }] }, 1);
427
+ assert.equal(bad.error, 'unknown status "later" (use pending, in_progress, blocked, done, or dropped)');
428
+ });
429
+
430
+ test("applyTodo rejects blocked without a note and leaves the list unchanged", () => {
431
+ const before = seeded();
432
+ for (const params of [
433
+ { action: "add", title: "Deploy", status: "blocked" },
434
+ { action: "update", id: 3, status: "blocked" },
435
+ { action: "write", tasks: [{ title: "Deploy", status: "blocked", note: " " }] },
436
+ ]) {
437
+ const result = applyTodo(before, params, 9);
438
+ assert.equal(result.error, "blocked needs a note naming what it waits for");
439
+ assert.equal(result.text, "Error: blocked needs a note naming what it waits for");
440
+ assert.strictEqual(result.state, before, `${params.action} must not change the list`);
441
+ }
442
+ const blocked = applyTodo(before, { action: "update", id: 3, status: "blocked", note: "waiting for the API contract" }, 9);
443
+ assert.equal(blocked.error, undefined);
444
+ assert.equal(blocked.text, "#3 Show git bash tails → blocked");
445
+ const unblocked = applyTodo(blocked.state, { action: "update", id: 3, status: "pending" }, 10);
446
+ assert.equal(unblocked.state.tasks[2].status, TODO_STATUS.PENDING);
447
+ });
448
+
449
+ test("dropped tasks never count as open: a list of done and dropped is finished, silent, and never stale", () => {
450
+ const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "A", status: "done" }, { title: "B", status: "dropped" }, { title: "C", status: "dropped" }] }, 1).state;
451
+ assert.deepEqual(todoSummary(state), { done: 1, total: 3, open: 0 });
452
+ assert.equal(todoStateMessage(state), undefined);
453
+ assert.equal(staleTurns(state, 9), 0);
454
+ });
455
+
456
+ test("blocked tasks stay open but alone never make the list stale; an actionable task still does", () => {
457
+ const blockedOnly = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "A", status: "done" }, { title: "Deploy", status: "blocked", note: "waiting for an admin" }] }, 1).state;
458
+ assert.deepEqual(todoSummary(blockedOnly), { done: 1, total: 2, open: 1 });
459
+ assert.equal(staleTurns(blockedOnly, 9), 0);
460
+ assert.match(todoStateMessage(blockedOnly) ?? "", /2\. \[blocked\] Deploy — waiting for an admin/);
461
+ const block = todoPromptBlock();
462
+ assert.match(block, /A blocked task waits on something outside the list: do not work on it until its condition holds/);
463
+ assert.match(block, /mark the tasks it made obsolete dropped instead of leaving them pending or marking them done/);
464
+ const mixed = applyTodo(blockedOnly, { action: "add", title: "Write docs" }, 1).state;
465
+ assert.equal(staleTurns(mixed, 3), 2);
466
+ });
467
+
468
+ test("renderTodoCard draws blocked with its reason and dropped dim without strikethrough, excluding dropped from the count", () => {
469
+ const state = applyTodo(
470
+ emptyTodo(),
471
+ { action: "write", tasks: [{ title: "Ship", status: "done" }, { title: "Old plan", status: "dropped" }, { title: "Deploy", status: "blocked", note: "waiting for an admin" }, { title: "Docs" }] },
472
+ 1,
473
+ ).state;
474
+ const plain = renderTodoCard(state, plainTheme, 60, { collapsed: false, staleTurns: 0 }).map(stripAnsi);
475
+ assert.match(plain[0], /Todos ▾ Collapse · 1 of 3/);
476
+ assert.match(plain[2], /^│ ✕ Old plan +│$/);
477
+ assert.match(plain[3], /^│ ⊘ Deploy · waiting for an admin +│$/);
478
+ const roles: string[] = [];
479
+ const spy = { ...plainTheme, fg(color: string, text: string) { roles.push(`${color}:${text}`); return text; } };
480
+ renderTodoCard(state, spy, 60, { collapsed: false, staleTurns: 0 });
481
+ assert.ok(roles.includes("muted:✕") && roles.includes("dim:Old plan"), "dropped: muted glyph, dim title");
482
+ assert.ok(roles.includes("warning:⊘"), "blocked: warning glyph");
483
+ });
484
+
485
+ test("the collapsed card skips blocked tasks for the next one, and shows a blocked task only when nothing else is open", () => {
486
+ const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "Deploy", status: "blocked", note: "waiting for an admin" }, { title: "Docs" }] }, 1).state;
487
+ assert.match(renderTodoCard(state, plainTheme, 60, { collapsed: true, staleTurns: 0 }).map(stripAnsi)[1], /^│ ○ Docs +│$/);
488
+ const onlyBlocked = applyTodo(state, { action: "update", id: 2, status: "done" }, 2).state;
489
+ assert.match(renderTodoCard(onlyBlocked, plainTheme, 60, { collapsed: true, staleTurns: 0 }).map(stripAnsi)[1], /^│ ⊘ Deploy · waiting for an admin +│$/);
490
+ });
491
+
492
+ test("renderTodoCard folds dropped tasks next to done ones in a long list", () => {
493
+ const tasks = Array.from({ length: 20 }, (_, index) => ({ title: `Task ${index + 1}`, status: index < 5 ? "done" : index < 8 ? "dropped" : "pending" }));
494
+ const state = applyTodo(emptyTodo(), { action: "write", tasks }, 1).state;
495
+ const plain = renderTodoCard(state, plainTheme, 60, { collapsed: false, staleTurns: 0 }).map(stripAnsi);
496
+ assert.equal(plain.length, 14);
497
+ assert.match(plain[0], /Todos ▾ Collapse · 5 of 17/);
498
+ assert.match(plain[1], /^│ ✓ 5 done +│$/);
499
+ assert.match(plain[2], /^│ ✕ 3 dropped +│$/);
500
+ assert.match(plain[3], /^│ ○ Task 9 +│$/);
501
+ assert.match(plain[12], /^│ … 3 more +│$/);
502
+ });
@@ -22,7 +22,7 @@ function payload(percent: number) {
22
22
  test("UsageView frames the panel, keeps every line at width, and shows the empty state", () => {
23
23
  const store = new UsageStore();
24
24
  const events: string[] = [];
25
- const view = new UsageView(store, { theme: plainTheme, now: () => NOW, active: () => undefined, onRefresh: async () => events.push("refresh"), onClose: () => events.push("close"), requestRender: () => events.push("render") });
25
+ const view = new UsageView(store, { theme: plainTheme, now: () => NOW, active: () => undefined, onRefresh: async () => { events.push("refresh"); }, onClose: () => events.push("close"), requestRender: () => events.push("render") });
26
26
  const empty = view.render(90).map(stripAnsi);
27
27
  assert.match(empty[0], /^╭─ ✿ Subscriptions ─+╮$/);
28
28
  assert.match(empty[1], /No subscription usage yet/);
@@ -122,6 +122,41 @@ test("parseCodexHeaders reads the SSE rate-limit headers when a provider sends t
122
122
  assert.equal(parseCodexHeaders({ "content-type": "text/event-stream" }, NOW), undefined);
123
123
  });
124
124
 
125
+ test("parseCodexHeaders ignores windows without a valid duration instead of inventing 0m", () => {
126
+ for (const minutes of [undefined, "", " ", "0", "-1", "invalid", "300oops", "Infinity", "1e309"]) {
127
+ const headers: Record<string, string> = { "x-codex-primary-used-percent": "0" };
128
+ if (minutes !== undefined) headers["x-codex-primary-window-minutes"] = minutes;
129
+ assert.equal(parseCodexHeaders(headers, NOW), undefined, `minutes: ${String(minutes)}`);
130
+ }
131
+ });
132
+
133
+ test("parseCodexHeaders keeps genuine zero usage and only the valid provider windows", () => {
134
+ const usage = parseCodexHeaders({
135
+ "x-codex-primary-used-percent": "0",
136
+ "x-codex-primary-window-minutes": "0",
137
+ "x-codex-secondary-used-percent": "0",
138
+ "x-codex-secondary-window-minutes": "10080",
139
+ }, NOW);
140
+ assert.ok(usage);
141
+ assert.deepEqual(usage.limits[0].windows.map((w) => `${w.label}:${w.usedPercent}`), ["week:0"]);
142
+ assert.equal(renderUsageBar(usage, plainTheme), "codex week ▱▱▱▱▱▱▱▱ 0%");
143
+ assert.doesNotMatch(renderUsageBar(usage, plainTheme)!, /0m|5h/);
144
+ });
145
+
146
+ test("parseCodexUsage filters invalid numeric windows without inventing a five-hour quota", () => {
147
+ for (const seconds of [undefined, 0, -1, Number.NaN, Number.POSITIVE_INFINITY, "18000"]) {
148
+ const usage = parseCodexUsage({ rate_limit: {
149
+ primary_window: { used_percent: 0, limit_window_seconds: seconds },
150
+ secondary_window: { used_percent: 0, limit_window_seconds: 604_800 },
151
+ } }, NOW);
152
+ assert.deepEqual(usage.limits[0].windows.map((w) => `${w.label}:${w.usedPercent}`), ["week:0"], `seconds: ${String(seconds)}`);
153
+ }
154
+ for (const used of [undefined, Number.NaN, Number.POSITIVE_INFINITY]) {
155
+ assert.deepEqual(parseCodexUsage({ rate_limit: { primary_window: { used_percent: used, limit_window_seconds: 604_800 } } }, NOW).limits, []);
156
+ }
157
+ assert.deepEqual(parseCodexUsage({ rate_limit: { primary_window: { used_percent: 0, limit_window_seconds: 0 } } }, NOW).limits, []);
158
+ });
159
+
125
160
  test("accountIdFromToken decodes the chatgpt account claim from an OAuth JWT", () => {
126
161
  const claims = Buffer.from(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acct-123" } })).toString("base64url");
127
162
  assert.equal(accountIdFromToken(`header.${claims}.sig`), "acct-123");
@@ -0,0 +1,49 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import test from "node:test";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { createJobRegistry } from "../lib/background-jobs.ts";
6
+ import { registerBackgroundJobTools } from "../lib/background-jobs-tools.ts";
7
+
8
+ // Guidance contract, not runtime routing or a performance guarantee.
9
+ const tools: Array<{ name: string; description: string; parameters: unknown }> = [];
10
+ registerBackgroundJobTools({ registerTool: (tool) => tools.push(tool), registerMessageRenderer: () => {} } as unknown as ExtensionAPI, {
11
+ registry: createJobRegistry({
12
+ outputDir: () => { throw new Error("Guidance contracts must not write job logs"); },
13
+ now: () => 0,
14
+ shell: () => { throw new Error("Guidance contracts must not execute jobs"); },
15
+ onSettled: () => {},
16
+ }),
17
+ sessionId: () => "contract", now: () => 0,
18
+ });
19
+ const background = tools.find((tool) => tool.name === "bash_background")!;
20
+ const core = readFileSync(new URL("../assets/orchestrator.md", import.meta.url), "utf8");
21
+ const guide = readFileSync(new URL("../docs/gentle-shell.md", import.meta.url), "utf8");
22
+
23
+ for (const [name, text] of Object.entries({ registeredDescription: background.description, guide })) {
24
+ test(`${name}: synchronous checks have an inclusive explicit 30-second limit and safe fallback`, () => {
25
+ for (const clause of [
26
+ "local, finite, deterministic checks", "reasonably expected to finish within 30 seconds",
27
+ "explicit timeout <= 30 seconds (30 included)",
28
+ "longer or unknown duration, external waits, CI, builds, and servers",
29
+ "Report a timeout as failure; never automatically retry, restart, or migrate it to background",
30
+ "Preserve RED/GREEN and public-check exit codes, stdout, and stderr",
31
+ "Engram bugfix saves and session summaries", "RDD", "not a performance guarantee",
32
+ ]) assert.ok(text.includes(clause), `${name} missing: ${clause}`);
33
+ });
34
+ }
35
+
36
+ test("inline still means the same parent, and the public background API stays asynchronous", () => {
37
+ assert.ok(core.includes("Small path: inline (parent; `bash_background` policy);"));
38
+ assert.ok(guide.includes("Inline means the same parent, not necessarily synchronous bash"));
39
+ assert.ok(core.includes("run the focused test and the suite inline, once each"));
40
+ assert.match(background.description, /return a job id at once/);
41
+ assert.match(background.description, /notified once, automatically/);
42
+ assert.match(background.description, /do not poll, sleep, or delegate a subagent to wait/);
43
+ assert.deepEqual(background.parameters, {
44
+ type: "object", additionalProperties: false, required: ["command"], properties: {
45
+ command: { type: "string", description: "Shell command to run in the background, in the session working directory." },
46
+ label: { type: "string", maxLength: 80, description: "Short human-readable label, e.g. 'CI for PR 1834'." },
47
+ },
48
+ });
49
+ });
@@ -0,0 +1,119 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { pathToFileURL } from "node:url";
8
+
9
+ const extensionUrl = pathToFileURL(join(import.meta.dirname, "..", "extensions", "skill-registry.ts")).href;
10
+
11
+ for (const code of ["ENOENT", "EMFILE"]) {
12
+ test(`registered skill watcher survives asynchronous ${code} and preserves other roots`, (t) => {
13
+ const root = mkdtempSync(join(tmpdir(), "gentle-skill-watch-error-"));
14
+ t.after(() => rmSync(root, { recursive: true, force: true }));
15
+ const child = spawnSync(process.execPath, ["--experimental-strip-types", "--input-type=module", "-e", `
16
+ import assert from "node:assert/strict";
17
+ import fs from "node:fs";
18
+ import { syncBuiltinESMExports } from "node:module";
19
+ import { join } from "node:path";
20
+ const root = ${JSON.stringify(root)};
21
+ const cwd = join(root, "workspace");
22
+ const home = join(root, "home");
23
+ process.env.HOME = home;
24
+ process.env.USERPROFILE = home;
25
+ delete process.env.GENTLE_PI_AGENTS_CHILD;
26
+ delete process.env.GENTLE_PI_NO_SKILL_REGISTRY;
27
+ const userRoot = join(home, ".codex", "skills");
28
+ const assets = join(userRoot, "imagegen", "assets");
29
+ const projectSkill = join(cwd, "skills", "local", "SKILL.md");
30
+ const userSkill = join(userRoot, "imagegen", "SKILL.md");
31
+ fs.mkdirSync(assets, { recursive: true });
32
+ fs.mkdirSync(join(cwd, "skills", "local"), { recursive: true });
33
+ fs.writeFileSync(projectSkill, "---\\nname: local\\ndescription: Initial project skill.\\n---\\n");
34
+ fs.writeFileSync(userSkill, "---\\nname: imagegen\\ndescription: Initial user skill.\\n---\\n");
35
+ const nativeWatch = fs.watch;
36
+ let userWatcher;
37
+ fs.watch = function (path, options, listener) {
38
+ const watcher = nativeWatch(path, options, listener);
39
+ if (path === userRoot && options?.recursive) userWatcher = watcher;
40
+ return watcher;
41
+ };
42
+ syncBuiltinESMExports();
43
+ const { default: skillRegistry, __testing } = await import(${JSON.stringify(extensionUrl)});
44
+ const events = new Map();
45
+ const commands = new Map();
46
+ const notices = [];
47
+ skillRegistry({
48
+ on: (name, handler) => events.set(name, handler),
49
+ registerCommand: (name, command) => commands.set(name, command),
50
+ registerFlag: () => {},
51
+ getFlag: () => false,
52
+ });
53
+ const ctx = { cwd, hasUI: true, ui: { notify: (message, level) => notices.push({ message, level }) } };
54
+ const waitFor = async (predicate) => {
55
+ const deadline = Date.now() + 5000;
56
+ while (!predicate() && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 25));
57
+ assert.ok(predicate(), "timed out waiting for watcher outcome");
58
+ };
59
+ try {
60
+ await events.get("session_start")(undefined, ctx);
61
+ await __testing.startupSettled(); // Startup regeneration runs after session_start returns.
62
+ assert.ok(userWatcher, "fixture must start the real recursive user watcher");
63
+ const initialWatchers = __testing.activeWatcherCount();
64
+ assert.equal(initialWatchers, 2);
65
+ const code = ${JSON.stringify(code)};
66
+ if (code === "ENOENT" && process.platform !== "darwin" && process.platform !== "win32") {
67
+ // Force the transient missing-directory race at Node's real recursive
68
+ // readdir boundary, after watcher creation, not in our refresh callback.
69
+ const nativeReaddir = fs.readdirSync;
70
+ let reachedRescan = false;
71
+ fs.readdirSync = function (path, options) {
72
+ if (path === assets) {
73
+ fs.readdirSync = nativeReaddir;
74
+ reachedRescan = true;
75
+ throw Object.assign(new Error("ENOENT: transient skill assets disappeared"), { code, syscall: "scandir", path });
76
+ }
77
+ return nativeReaddir(path, options);
78
+ };
79
+ fs.writeFileSync(join(assets, "trigger.txt"), "trigger directory rescan");
80
+ await waitFor(() => reachedRescan);
81
+ // Newer Node versions swallow rescan ENOENT themselves. Still
82
+ // exercise our public error listener without requiring Node's bug.
83
+ if (!notices.some(({ message, level }) => level === "warning" && message.includes(code))) {
84
+ userWatcher.emit("error", Object.assign(new Error("ENOENT: skill watcher failed"), { code }));
85
+ }
86
+ } else {
87
+ // Native Darwin/Windows recursion has no JS readdir boundary;
88
+ // inject its public error event, as for asynchronous resource errors.
89
+ setImmediate(() => userWatcher.emit("error", Object.assign(new Error(code + ": skill watcher failed"), { code })));
90
+ }
91
+ await waitFor(() => notices.some(({ message, level }) => level === "warning" && message.includes(code)));
92
+ assert.equal(__testing.activeWatcherCount(), initialWatchers - 1, "only the failed root must stop");
93
+ assert.ok(notices.some(({ message }) => message.includes("/skill-registry:refresh")));
94
+ fs.writeFileSync(projectSkill, "---\\nname: local\\ndescription: Changed project skill.\\n---\\n");
95
+ const registry = join(cwd, ".atl", "skill-registry.md");
96
+ await waitFor(() => fs.readFileSync(registry, "utf8").includes("Changed project skill"));
97
+ fs.writeFileSync(userSkill, "---\\nname: imagegen\\ndescription: Manually changed user skill.\\n---\\n");
98
+ await commands.get("skill-registry:refresh").handler("", ctx);
99
+ assert.match(fs.readFileSync(registry, "utf8"), /Manually changed user skill/);
100
+ } finally {
101
+ await events.get("session_shutdown")(undefined, ctx);
102
+ }
103
+ assert.equal(__testing.activeWatcherCount(), 0);
104
+ await events.get("session_start")(undefined, ctx);
105
+ await __testing.startupSettled();
106
+ assert.equal(__testing.activeWatcherCount(), 2, "a later session must rewatch the failed root");
107
+ await events.get("session_shutdown")(undefined, ctx);
108
+ assert.equal(__testing.activeWatcherCount(), 0);
109
+ console.log("SURVIVED ${code}");
110
+ `], {
111
+ encoding: "utf8",
112
+ timeout: 15_000,
113
+ env: { ...process.env, GIT_CEILING_DIRECTORIES: root },
114
+ });
115
+ assert.equal(child.error, undefined, child.error?.message);
116
+ assert.equal(child.status, 0, child.stderr);
117
+ assert.match(child.stdout, new RegExp(`SURVIVED ${code}`));
118
+ });
119
+ }