gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,11 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
- import { VisualCustomizeView } from "../lib/visual-customize-view.ts";
3
+ import { VisualCustomizeView, type CustomizeInline, type CustomizeRow } from "../lib/visual-customize-view.ts";
4
4
  import { CommandPalette } from "../lib/command-palette.ts";
5
5
  import { visibleWidth } from "@earendil-works/pi-tui";
6
6
  import { getThemeByName, Theme } from "../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js";
7
7
 
8
8
  const theme = { fg: (_role: string, text: string) => text };
9
+ type FoldableRow = CustomizeRow & { visible?: () => boolean; keyhint?: string };
9
10
 
10
11
  test("Customize shares Commands card geometry, header and focused-row style at wide, narrow and short sizes", () => {
11
12
  for (const [width, height] of [[100, 24], [36, 12], [76, 5]] as const) {
@@ -19,7 +20,7 @@ test("Customize shares Commands card geometry, header and focused-row style at w
19
20
  assert.equal(customize.at(-1), commands.at(-1));
20
21
  assert.ok(customize.every(line => visibleWidth(line) === width));
21
22
  assert.ok(customize.length <= height);
22
- assert.match(customize.join("\n"), /Visual customization.*esc/);
23
+ assert.match(customize.join("\n"), /Customization.*esc/);
23
24
  assert.match(customize.join("\n"), /▸ Animations/);
24
25
  if (height > 5) {
25
26
  view.handleInput("\x1b[C");
@@ -117,7 +118,7 @@ test("profile name submission never accepts a hidden field at heights 3–5, inc
117
118
 
118
119
  test("profile preview and cancel never apply; keyboard name and confirmations require deliberate input", async () => {
119
120
  const events: string[] = [];
120
- const profile = { name: "Desk", themeName: "Light Theme", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false } } };
121
+ const profile = { name: "Desk", themeName: "Light Theme", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false, contextWarning: false } } };
121
122
  const view = new VisualCustomizeView({ rows: [], theme, requestRender: () => {}, onClose: () => {}, profiles: {
122
123
  list: () => [profile], save: (name, replace) => { events.push(`save:${name}:${replace}`); }, apply: (name) => { events.push(`apply:${name}`); }, delete: (name) => { events.push(`delete:${name}`); }, reset: () => { events.push("reset"); },
123
124
  } });
@@ -126,6 +127,7 @@ test("profile preview and cancel never apply; keyboard name and confirmations re
126
127
  assert.match(view.render(100).join("\n"), /potato.*cyan/);
127
128
  assert.match(view.render(100).join("\n"), /hidden.*below-input.*compact/);
128
129
  assert.match(view.render(100).join("\n"), /changes:off.*rdd:off.*agents:on.*todo:off.*usageCost:on.*modelDetails:off/);
130
+ assert.match(view.render(120).join("\n"), /changes:off.*rdd:off.*agents:on.*todo:off.*usageCost:on.*modelDetails:off.*contextWarning:off/);
129
131
  view.handleInput("a"); view.handleInput("\x1b");
130
132
  assert.deepEqual(events, []);
131
133
  view.handleInput("p"); view.handleInput("s"); view.handleInput("New"); view.handleInput("\x7f"); view.handleInput("w\r");
@@ -145,7 +147,7 @@ test("profile preview and cancel never apply; keyboard name and confirmations re
145
147
 
146
148
  test("profile confirmation stays visible at six rows for every destructive or applying action", async () => {
147
149
  const events: string[] = [];
148
- const profile = { name: "Desk", themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false } } };
150
+ const profile = { name: "Desk", themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false, contextWarning: false } } };
149
151
  const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => 6, requestRender: () => {}, onClose: () => {}, profiles: {
150
152
  list: () => [profile], save: (name, replace) => { events.push(`save:${name}:${replace}`); }, apply: (name) => { events.push(`apply:${name}`); }, delete: (name) => { events.push(`delete:${name}`); }, reset: () => { events.push("reset"); },
151
153
  } });
@@ -163,7 +165,7 @@ test("profile confirmation stays visible at six rows for every destructive or ap
163
165
 
164
166
  test("profile confirmation captures selected name across catalog mutation and cancels vanished targets", async () => {
165
167
  const events: string[] = [];
166
- const base = { themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false } } };
168
+ const base = { themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false, contextWarning: false } } };
167
169
  let catalog = [{ ...base, name: "Desk" }, { ...base, name: "Mobile" }];
168
170
  const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => 6, requestRender: () => {}, onClose: () => {}, profiles: {
169
171
  list: () => catalog, save: (name, replace) => { events.push(`save:${name}:${replace}`); }, apply: (name) => { events.push(`apply:${name}`); }, delete: (name) => { events.push(`delete:${name}`); }, reset: () => { events.push("reset"); },
@@ -188,7 +190,7 @@ test("profile confirmation captures selected name across catalog mutation and ca
188
190
  test("unrendered or clipped confirmations cannot be accepted", () => {
189
191
  const events: string[] = [];
190
192
  let height = 6;
191
- const base = { name: "Desk", themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false } } };
193
+ const base = { name: "Desk", themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false, contextWarning: false } } };
192
194
  const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => height, requestRender: () => {}, onClose: () => {}, profiles: {
193
195
  list: () => [base], save: () => {}, apply: () => {}, delete: () => { events.push("delete"); }, reset: () => { events.push("reset"); },
194
196
  } });
@@ -202,7 +204,7 @@ test("unrendered or clipped confirmations cannot be accepted", () => {
202
204
 
203
205
  test("profile confirmations cannot execute after an unrendered resize hides full identity and yes", () => {
204
206
  const name = "A".repeat(64);
205
- const profile = { name, themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true } } };
207
+ const profile = { name, themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true } } };
206
208
  for (const [key, action] of [["a", "apply"], ["d", "delete"], ["r", "replace"], ["z", "reset"]] as const) {
207
209
  for (const width of [100, 36]) for (const shortHeight of [1, 2]) {
208
210
  let height = 12;
@@ -224,7 +226,7 @@ test("profile confirmations cannot execute after an unrendered resize hides full
224
226
  });
225
227
 
226
228
  test("profile confirmation refuses a changed target identity without another render", () => {
227
- let catalog = [{ name: "First", themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true } } }];
229
+ let catalog = [{ name: "First", themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true } } }];
228
230
  const effects: string[] = [];
229
231
  const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => 12, requestRender: () => {}, onClose: () => {}, profiles: { list: () => catalog, save: () => {}, apply: name => { effects.push(name); }, delete: () => {}, reset: () => {} } });
230
232
  view.handleInput("p"); view.handleInput("a");
@@ -261,7 +263,7 @@ test("main controls require a visible selected row at the last rendered width an
261
263
  });
262
264
 
263
265
  test("profile confirmation refuses same-name content drift for apply, replace and delete", () => {
264
- const original = { name: "Desk", themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true } } };
266
+ const original = { name: "Desk", themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true } } };
265
267
  for (const key of ["a", "r", "d"]) {
266
268
  let catalog = [original];
267
269
  const effects: string[] = [];
@@ -311,7 +313,7 @@ test("category panel renders bounded wide and narrow frames with independent nav
311
313
  test("long profile confirmation displays complete identity and yes instruction before accepting", async () => {
312
314
  const name = "A".repeat(64);
313
315
  const events: string[] = [];
314
- const profile = { name, themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true } } };
316
+ const profile = { name, themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true } } };
315
317
  let height = 8;
316
318
  const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => height, requestRender: () => {}, onClose: () => {}, profiles: { list: () => [profile], save: () => {}, apply: value => { events.push(value); }, delete: () => {}, reset: () => {} } });
317
319
  view.handleInput("p");
@@ -416,3 +418,279 @@ test("view awaits async actions, reports errors and repaints after completion",
416
418
  assert.deepEqual(errors, ["write failed"]);
417
419
  assert.ok(renders >= 2);
418
420
  });
421
+
422
+ // --- inline bridge (direct rows, no nested native dialogs) --------------------
423
+
424
+ const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
425
+
426
+ function inlineView(action: (inline: CustomizeInline) => void | Promise<void>, options: { rowsAvailable?: () => number; onClose?: () => void } = {}) {
427
+ return new VisualCustomizeView({ rows: [{ label: "Field row", action }], theme, rowsAvailable: options.rowsAvailable ?? (() => 12), requestRender: () => {}, onClose: options.onClose ?? (() => {}) });
428
+ }
429
+
430
+ test("Notifications header reads Audio notifications while other panes read Customization", () => {
431
+ const view = new VisualCustomizeView({ rows: [
432
+ { category: "Animations", label: "Speed", action: () => {} },
433
+ { category: "Notifications", label: "Audio notifications: off", action: () => {} },
434
+ ], theme, requestRender: () => {}, onClose: () => {} });
435
+ assert.equal(view.title(), "Customization");
436
+ assert.match(view.render(76).join("\n"), /Customization.*esc/);
437
+ view.handleInput("\x1b[B");
438
+ assert.equal(view.title(), "Audio notifications");
439
+ assert.match(view.render(76).join("\n"), /Audio notifications.*esc/);
440
+ });
441
+
442
+ test("inline field accepts typed, pasted and Kitty input, submits on Enter and cancels with Escape without closing", async () => {
443
+ let closed = 0;
444
+ const submitted: string[] = [];
445
+ const view = inlineView(async inline => {
446
+ const value = await inline.input({ prompt: "Path", value: "" });
447
+ if (value !== undefined) submitted.push(value);
448
+ }, { onClose: () => { closed++; } });
449
+ view.render(76);
450
+ view.handleInput("\r");
451
+ await tick();
452
+ assert.match(view.render(76).join("\n"), /Path:/);
453
+ view.handleInput("a");
454
+ view.handleInput("\x1b[200~b c\x1b[201~");
455
+ view.handleInput("\x1b[100u");
456
+ assert.match(view.render(76).join("\n"), /Path: ab cd/);
457
+ view.handleInput("\x1b");
458
+ await tick();
459
+ assert.equal(closed, 0, "first Escape cancels the field, not the whole card");
460
+ assert.deepEqual(submitted, []);
461
+ assert.doesNotMatch(view.render(76).join("\n"), /Path:/);
462
+ view.handleInput("\x1b");
463
+ assert.equal(closed, 1, "second Escape closes the card");
464
+ });
465
+
466
+ test("inline field submits a visible value and drops invisible resizes without writing", async () => {
467
+ let height = 12;
468
+ const submitted: string[] = [];
469
+ const view = inlineView(async inline => {
470
+ const value = await inline.input({ prompt: "Path", value: "" });
471
+ if (value !== undefined) submitted.push(value);
472
+ }, { rowsAvailable: () => height });
473
+ view.render(76);
474
+ view.handleInput("\r");
475
+ await tick();
476
+ view.handleInput("/local/a.wav");
477
+ view.render(76);
478
+ view.handleInput("\r");
479
+ await tick();
480
+ assert.deepEqual(submitted, ["/local/a.wav"]);
481
+
482
+ const hidden = inlineView(async inline => {
483
+ const value = await inline.input({ prompt: "Path", value: "x" });
484
+ if (value !== undefined) submitted.push(value);
485
+ }, { rowsAvailable: () => height });
486
+ hidden.render(76);
487
+ hidden.handleInput("\r");
488
+ await tick();
489
+ height = 3; // Resize below the preview area without another render.
490
+ hidden.handleInput("\r");
491
+ await tick();
492
+ assert.deepEqual(submitted, ["/local/a.wav"], "an Enter that cannot see the field must not submit");
493
+ });
494
+
495
+ test("inline confirmation accepts only a fully visible yes and resolves false otherwise", async () => {
496
+ let height = 12;
497
+ const seen: boolean[] = [];
498
+ const view = inlineView(async inline => { seen.push(await inline.confirm("Replace invalid audio configuration? The existing file is malformed or unreadable.")); }, { rowsAvailable: () => height });
499
+ view.render(76);
500
+ view.handleInput("\r");
501
+ await tick();
502
+ assert.match(view.render(76).join("\n"), /y yes/);
503
+ view.handleInput("n");
504
+ await tick();
505
+ assert.deepEqual(seen, [false]);
506
+
507
+ const second = inlineView(async inline => { seen.push(await inline.confirm("Replace invalid audio configuration? The existing file is malformed or unreadable.")); }, { rowsAvailable: () => height });
508
+ second.render(76);
509
+ second.handleInput("\r");
510
+ await tick();
511
+ second.handleInput("y");
512
+ await tick();
513
+ assert.deepEqual(seen, [false, true]);
514
+
515
+ const short = inlineView(async inline => { seen.push(await inline.confirm("Replace invalid audio configuration? The existing file is malformed or unreadable.")); }, { rowsAvailable: () => 5 });
516
+ short.render(76);
517
+ short.handleInput("\r");
518
+ await tick();
519
+ short.handleInput("y");
520
+ await tick();
521
+ assert.deepEqual(seen, [false, true, false], "a confirmation with no visible yes must fail closed");
522
+ });
523
+
524
+ test("dispose resolves a pending inline field as cancelled without writing", async () => {
525
+ let observed: CustomizeInline | undefined;
526
+ const submitted: string[] = [];
527
+ const view = inlineView(async inline => {
528
+ observed = inline;
529
+ const value = await inline.input({ prompt: "Path", value: "" });
530
+ if (value !== undefined) submitted.push(value);
531
+ });
532
+ view.render(76);
533
+ view.handleInput("\r");
534
+ await tick();
535
+ view.dispose();
536
+ await tick();
537
+ assert.deepEqual(submitted, []);
538
+ assert.equal(observed!.disposed, true, "the bridge signals disposal so rows cannot persist");
539
+ view.handleInput("\x1b");
540
+ assert.deepEqual(submitted, [], "a disposed view ignores further input");
541
+ });
542
+
543
+ test("row key shortcuts run before panel shortcuts and are ignored outside the controls pane", () => {
544
+ const keys: string[] = [];
545
+ const view = new VisualCustomizeView({ rows: [{ category: "Notifications", label: "event", action: () => {}, key: (data) => { if (data === "p") { keys.push("p"); return true; } return false; } }],
546
+ theme, requestRender: () => {}, onClose: () => {}, profiles: { list: () => [], save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
547
+ view.render(76);
548
+ // Categories pane: the row key is not consulted, so the panel handles p.
549
+ view.handleInput("p");
550
+ assert.deepEqual(keys, []);
551
+ view.handleInput("\x1b");
552
+ view.handleInput("\x1b[C");
553
+ view.handleInput("p");
554
+ assert.deepEqual(keys, ["p"], "the highlighted row consumes p instead of opening profiles");
555
+ });
556
+
557
+ test("Notifications non-event controls consume p without opening visual profiles and keep the audio title", () => {
558
+ const listed: string[] = [];
559
+ const rows: CustomizeRow[] = [
560
+ { category: "Notifications", label: "Audio notifications: on", action: () => {} },
561
+ { category: "Notifications", label: "Audio: unmuted", action: () => {} },
562
+ { category: "Notifications", label: "Advanced: show per-event exceptions", action: () => {} },
563
+ ];
564
+ const view = new VisualCustomizeView({ rows, theme, requestRender: () => {}, onClose: () => {}, profiles: { list: () => { listed.push("list"); return []; }, save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
565
+ view.render(76);
566
+ view.handleInput("\x1b[C");
567
+ for (let i = 0; i < rows.length; i++) {
568
+ view.handleInput("p");
569
+ const frame = view.render(76).join("\n");
570
+ assert.equal(view.title(), "Audio notifications", `non-event row ${i} must keep the audio title`);
571
+ assert.match(frame, /Audio notifications/, `non-event row ${i} must stay on the audio card`);
572
+ assert.doesNotMatch(frame, /Visual profiles/, `non-event row ${i} must not open the profile pane`);
573
+ if (i < rows.length - 1) view.handleInput("\x1b[B");
574
+ }
575
+ assert.deepEqual(listed, [], "the profile catalog must never be listed from Notifications controls");
576
+ });
577
+
578
+ test("p still opens visual profiles from a non-Notifications control", () => {
579
+ const listed: string[] = [];
580
+ const view = new VisualCustomizeView({ rows: [{ category: "Themes", label: "Dark", action: () => {} }], theme, requestRender: () => {}, onClose: () => {}, profiles: { list: () => { listed.push("list"); return []; }, save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
581
+ view.render(76);
582
+ view.handleInput("\x1b[C");
583
+ view.handleInput("p");
584
+ assert.match(view.render(76).join("\n"), /Visual profiles/);
585
+ assert.equal(listed.length, 1);
586
+ });
587
+
588
+ test("the Notifications card footer is audio-specific and never advertises visual profiles", () => {
589
+ const profiles = { list: () => [], save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} };
590
+ const view = new VisualCustomizeView({ rows: [
591
+ { category: "Animations", label: "Speed", action: () => {} },
592
+ { category: "Notifications", label: "Audio notifications: off", action: () => {} },
593
+ ], theme, requestRender: () => {}, onClose: () => {}, rowsAvailable: () => 24, profiles });
594
+ view.handleInput("\x1b[B");
595
+ view.handleInput("\x1b[C");
596
+ for (const width of [60, 80]) {
597
+ const frame = view.render(width).join("\n");
598
+ assert.match(frame, /Tab panes · Enter change · p test · Esc close/, `basic audio footer at width ${width}`);
599
+ assert.doesNotMatch(frame, /f WAV/, `basic audio footer must not advertise file editing at width ${width}`);
600
+ assert.doesNotMatch(frame, /p profiles/, `audio footer must not advertise profiles at width ${width}`);
601
+ }
602
+ const other = new VisualCustomizeView({ rows: [{ category: "Themes", label: "Dark", action: () => {} }], theme, requestRender: () => {}, onClose: () => {}, rowsAvailable: () => 24, profiles });
603
+ other.handleInput("\x1b[C");
604
+ assert.match(other.render(80).join("\n"), /p profiles/, "other categories keep the profiles hint");
605
+ });
606
+
607
+ test("an async row key task holds busy until it settles and releases it after a rejection", async () => {
608
+ const errors: Error[] = [];
609
+ const keyed: string[] = [];
610
+ let actions = 0;
611
+ const view = new VisualCustomizeView({ rows: [{
612
+ category: "Notifications", label: "Async row", action: () => { actions++; },
613
+ key: data => { if (data === "f") { keyed.push("f"); return Promise.reject(new Error("key failed")); } return false; },
614
+ }], theme, requestRender: () => {}, onClose: () => {}, onError: error => errors.push(error), rowsAvailable: () => 12 });
615
+ view.render(76);
616
+ view.handleInput("\x1b[C");
617
+ view.handleInput("f");
618
+ view.handleInput("f");
619
+ assert.deepEqual(keyed, ["f"], "a second key is ignored while the async task is busy");
620
+ await tick(); await tick();
621
+ assert.equal(errors.length, 1, "a rejected key surfaces through onError");
622
+ assert.equal(errors[0]?.message, "key failed");
623
+ view.handleInput("\r");
624
+ await tick();
625
+ assert.equal(actions, 1, "busy is released after the rejected key task settles");
626
+ });
627
+
628
+ test("a visible() false row is unreachable and its action or key is never invoked", async () => {
629
+ const actions: string[] = [];
630
+ const keys: string[] = [];
631
+ let advanced = false;
632
+ const rows: FoldableRow[] = [
633
+ { category: "Notifications" as const, label: "Basic", action: () => { actions.push("basic"); } },
634
+ { category: "Notifications" as const, label: "Hidden detail", visible: () => advanced, action: () => { actions.push("hidden"); }, key: (data: string) => { keys.push(data); return true; } },
635
+ ];
636
+ const view = new VisualCustomizeView({ rows, theme, rowsAvailable: () => 12, requestRender: () => {}, onClose: () => {} });
637
+ view.render(76);
638
+ view.handleInput("\x1b[C");
639
+ for (let i = 0; i < 3; i++) { view.handleInput("\x1b[B"); assert.doesNotMatch(view.render(76).join("\n"), /Hidden detail/, "a collapsed row must not render"); }
640
+ view.handleInput("\r");
641
+ view.handleInput("f");
642
+ view.handleInput("p");
643
+ await tick();
644
+ assert.ok(!actions.includes("hidden"), "a hidden row action must never run");
645
+ assert.deepEqual(keys, [], "a hidden row key must never run");
646
+ advanced = true;
647
+ assert.match(view.render(76).join("\n"), /Hidden detail/, "expanding reveals the row");
648
+ });
649
+
650
+ test("selection clamps to a visible row after a collapse", async () => {
651
+ const actions: string[] = [];
652
+ let advanced = true;
653
+ const rows: FoldableRow[] = [
654
+ { category: "Notifications" as const, label: "Basic", action: () => { actions.push("basic"); } },
655
+ { category: "Notifications" as const, label: "Advanced", visible: () => advanced, action: () => { actions.push("advanced"); } },
656
+ ];
657
+ const view = new VisualCustomizeView({ rows, theme, rowsAvailable: () => 12, requestRender: () => {}, onClose: () => {} });
658
+ view.render(76);
659
+ view.handleInput("\x1b[C");
660
+ view.handleInput("\x1b[B");
661
+ assert.match(view.render(76).join("\n"), /\u25b8 Advanced/);
662
+ advanced = false;
663
+ assert.doesNotMatch(view.render(76).join("\n"), /Advanced/);
664
+ view.handleInput("\r");
665
+ await tick();
666
+ assert.deepEqual(actions, ["basic"], "the collapsed selection clamps to the visible row");
667
+ });
668
+
669
+ test("a throwing visible() callback hides its row instead of exposing it", async () => {
670
+ const actions: string[] = [];
671
+ const rows: FoldableRow[] = [
672
+ { category: "Notifications" as const, label: "Boom", visible: () => { throw new Error("boom"); }, action: () => { actions.push("boom"); } },
673
+ { category: "Notifications" as const, label: "Safe", action: () => { actions.push("safe"); } },
674
+ ];
675
+ const view = new VisualCustomizeView({ rows, theme, rowsAvailable: () => 12, requestRender: () => {}, onClose: () => {} });
676
+ view.render(76);
677
+ view.handleInput("\x1b[C");
678
+ assert.doesNotMatch(view.render(76).join("\n"), /Boom/, "a row whose visibility check throws must fail closed");
679
+ view.handleInput("\r");
680
+ await tick();
681
+ assert.deepEqual(actions, ["safe"], "the throwing row must never be activated");
682
+ });
683
+
684
+ test("the audio footer drops f WAV when the selected row has no keyhint and advertises it when it does", () => {
685
+ const view = new VisualCustomizeView({ rows: [
686
+ { category: "Notifications", label: "Audio notifications: off", action: () => {} },
687
+ { category: "Notifications", label: "Success: success tone", action: () => {}, keyhint: "Tab panes · Enter change · f WAV · p test · Esc close" },
688
+ ] as FoldableRow[], theme, rowsAvailable: () => 24, requestRender: () => {}, onClose: () => {}, profiles: { list: () => [], save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
689
+ view.handleInput("\x1b[C");
690
+ const basic = view.render(80).join("\n");
691
+ assert.match(basic, /Tab panes · Enter change · p test · Esc close/, "a row with no keyhint keeps the audio footer without a WAV hint");
692
+ assert.doesNotMatch(basic, /f WAV/, "the default audio footer must not advertise file editing");
693
+ view.handleInput("\x1b[B");
694
+ const advanced = view.render(80).join("\n");
695
+ assert.match(advanced, /Tab panes · Enter change · f WAV · p test · Esc close/, "a row with a keyhint advertises the WAV key");
696
+ });
@@ -58,17 +58,74 @@ function dispatchWriter(input: Record<string, unknown>) {
58
58
  cwd,
59
59
  hasUI: false,
60
60
  ui: { confirm: async () => true },
61
- } as ExtensionContext);
61
+ } as unknown as ExtensionContext);
62
62
  }
63
63
 
64
64
  async function assertAccepted(input: Record<string, unknown>, message: string) {
65
65
  assert.equal(await dispatchWriter(input), undefined, message);
66
66
  }
67
67
 
68
+ const RESEND = "Resend the same task text unchanged except for that section; never shorten or re-summarize it.";
69
+ const PROBLEM = /^(?:No `## Allowed edit surfaces` heading was found\.|The section has no entries\.|Repeated sections list different surfaces\.|Line ".+" is not a valid surface entry; move prose under a following Markdown heading\.|Line ".+" is not a valid surface entry; if it is a path, wrap the whole entry in backticks, otherwise move it under a following Markdown heading\.|Entry ".+" is not a narrow repository-relative path; remove absolute paths, `\.\.` segments, root globs, and stray backticks\.)$/;
70
+
71
+ // Every rejection is the canonical text, exactly one concrete problem, and the
72
+ // resend instruction; nothing else (review R3-005).
73
+ function rejectionProblem(reason: string): string {
74
+ assert.ok(reason.startsWith(`${REJECTION} `), "canonical rejection prefix");
75
+ assert.ok(reason.endsWith(` ${RESEND}`), "resend instruction suffix");
76
+ const problem = reason.slice(REJECTION.length + 1, reason.length - RESEND.length - 1);
77
+ assert.match(problem, PROBLEM);
78
+ return problem;
79
+ }
80
+
68
81
  async function assertRejected(input: Record<string, unknown>, message: string) {
69
- assert.deepEqual(await dispatchWriter(input), { block: true, reason: REJECTION }, message);
82
+ const result = await dispatchWriter(input);
83
+ assert.equal(result?.block, true, message);
84
+ rejectionProblem(result.reason);
70
85
  }
71
86
 
87
+ // gentle-shell#1713: an unexplained rejection made the orchestrator resend
88
+ // shorter, re-summarized tasks (1786 -> 1542 chars) and lose requirements.
89
+ test("rejection names the offending line and asks for the same task unchanged", async () => {
90
+ const result = await dispatchWriter({
91
+ agent: "gentle-ai-worker",
92
+ mode: "task",
93
+ task: [
94
+ "Implement split transactions.",
95
+ "",
96
+ "## Allowed edit surfaces",
97
+ "- src/model.ts",
98
+ "- test/**/*.test.ts",
99
+ "",
100
+ "Requirements: one parent transaction with ordered allocations.",
101
+ ].join("\n"),
102
+ });
103
+ assert.equal(result?.block, true);
104
+ assert.match(result.reason, /Line "Requirements: one parent transaction with ordered allocations\." is not a valid surface entry/);
105
+ assert.match(result.reason, /Resend the same task text unchanged except for that section; never shorten or re-summarize it\./);
106
+ const missing = await dispatchWriter({ agent: "gentle-ai-worker", mode: "task", task: "Implement it." });
107
+ assert.match(missing?.reason ?? "", /No `## Allowed edit surfaces` heading was found\./);
108
+ });
109
+
110
+ test("each rejection names its concrete problem, and invalid paths are not called prose", async () => {
111
+ const problem = async (input: Record<string, unknown>) => rejectionProblem((await dispatchWriter({ agent: "gentle-ai-worker", mode: "task", ...input }))!.reason);
112
+ assert.equal(await problem({ task: "## Allowed edit surfaces\n\n## Return\nReport" }), "The section has no entries.");
113
+ assert.equal(await problem({ task: "## Allowed edit surfaces\nsrc/a.ts", context: "## Allowed edit surfaces\nsrc/b.ts" }), "Repeated sections list different surfaces.");
114
+ for (const path of ["/etc/passwd", "../outside.ts", "src/../../x.ts", "**/*.ts", "src/`odd.ts"]) {
115
+ assert.equal(await problem({ task: `## Allowed edit surfaces\n- ${path}` }), `Entry "${path}" is not a narrow repository-relative path; remove absolute paths, \`..\` segments, root globs, and stray backticks.`, path);
116
+ }
117
+ const long = `Then ${"verify every requirement ".repeat(8)}carefully.`;
118
+ const ambiguous = (shown: string) => `Line "${shown}" is not a valid surface entry; if it is a path, wrap the whole entry in backticks, otherwise move it under a following Markdown heading.`;
119
+ assert.equal(await problem({ task: `## Allowed edit surfaces\nsrc/a.ts\n${long}` }), ambiguous(`${long.slice(0, 120)}...`));
120
+ // A real path with a space must not be steered out of the section.
121
+ assert.equal(await problem({ task: "## Allowed edit surfaces\n- docs/with space.md" }), ambiguous("- docs/with space.md"));
122
+ // Whitespace that stays invalid even when quoted is plain prose.
123
+ assert.equal(await problem({ task: "## Allowed edit surfaces\n- ~ notes for later" }), `Line "- ~ notes for later" is not a valid surface entry; move prose under a following Markdown heading.`);
124
+ const control = await problem({ task: "## Allowed edit surfaces\nsrc/a\u0007.ts" });
125
+ assert.doesNotMatch(control, /\u0007/);
126
+ assert.match(control, /^Entry "src\/a \.ts" is not a narrow repository-relative path/);
127
+ });
128
+
72
129
  test("task-scoped surfaces are accepted ahead of a deeper heading", async () => {
73
130
  await assertAccepted({
74
131
  agent: "gentle-ai-worker",