gentle-pi 3.7.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,655 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { buildNotificationRows } from "../lib/notification-customize.ts";
5
+ import { claimNotificationOwner, getNotificationService } from "../lib/notification-service.ts";
6
+ import { DEFAULT_NOTIFICATION_SETTINGS, type NotificationEvent, type NotificationSettings, type NotificationSound } from "../lib/notification-policy.ts";
7
+ import { VisualCustomizeView, type CustomizeInline, type CustomizeRow } from "../lib/visual-customize-view.ts";
8
+ import { fileURLToPath } from "node:url";
9
+ import { basename } from "node:path";
10
+
11
+ const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
12
+
13
+ /** Real packaged WAVs so a valid path uses an existing fixture, never a synthetic name. */
14
+ const WAV_FIXTURE = fileURLToPath(new URL("../assets/sounds/success.wav", import.meta.url));
15
+ const WAV_FIXTURES = {
16
+ success: fileURLToPath(new URL("../assets/sounds/success.wav", import.meta.url)),
17
+ error: fileURLToPath(new URL("../assets/sounds/error.wav", import.meta.url)),
18
+ attention: fileURLToPath(new URL("../assets/sounds/attention.wav", import.meta.url)),
19
+ } as const;
20
+ /** Fixed verified targets per basic group (S15): the group owns its own file, never a global preset. */
21
+ const GROUPS = [
22
+ { prefix: "Success:", fixture: WAV_FIXTURES.success, targets: ["agent.completed", "subagent.completed"] },
23
+ { prefix: "Error:", fixture: WAV_FIXTURES.error, targets: ["agent.failed", "subagent.failed", "subagent.timed_out"] },
24
+ { prefix: "Attention:", fixture: WAV_FIXTURES.attention, targets: ["agent.attention"] },
25
+ ] as const;
26
+ /** Friendly tone name expected for each default group. */
27
+ const GROUP_TONE: Readonly<Record<string, string>> = { "Success:": "success tone", "Error:": "error tone", "Attention:": "attention tone" };
28
+ function groupEvents(targets: readonly string[], sound: NotificationSound): Partial<Record<NotificationEvent, NotificationSound>> {
29
+ return Object.fromEntries(targets.map(target => [target, sound])) as Partial<Record<NotificationEvent, NotificationSound>>;
30
+ }
31
+
32
+ function harness(options: { malformed?: boolean; failWrite?: boolean; events?: Partial<Record<NotificationEvent, NotificationSound>> } = {}) {
33
+ const notes: string[] = [];
34
+ const nested = { select: 0, input: 0, confirm: 0 };
35
+ let writes = 0; let probes = 0; let played = 0;
36
+ let settings: NotificationSettings = structuredClone(DEFAULT_NOTIFICATION_SETTINGS);
37
+ if (options.events) settings.audio.events = structuredClone(options.events);
38
+ const ctx = { mode: "tui", hasUI: true, sessionManager: { getSessionId: () => "nc-session" }, ui: {
39
+ select: async () => { nested.select++; return undefined; },
40
+ input: async () => { nested.input++; return undefined; },
41
+ confirm: async () => { nested.confirm++; return false; },
42
+ notify: (text: string) => notes.push(text),
43
+ } } as unknown as ExtensionContext;
44
+ const owner = claimNotificationOwner(() => {}, { env: {},
45
+ read: () => ({ settings, source: "global_file", malformed: options.malformed ?? false, readError: false, globalFile: "/config/notifications.json" }),
46
+ write: (next: NotificationSettings) => { writes++; if (options.failWrite) throw Error("private"); settings = next; return "/config/notifications.json"; },
47
+ backend: { availability: async () => { probes++; return "available"; }, capabilities: async () => new Set(["wav", "ogg", "flac"] as const), play: async (_sound, _signal, permit) => { if (permit.start()) played++; } },
48
+ });
49
+ owner.attach(ctx); getNotificationService()!.setMuted(ctx, false);
50
+ return { ctx, owner, notes, nested, get writes() { return writes; }, get probes() { return probes; }, get played() { return played; },
51
+ rows: () => buildNotificationRows(ctx) };
52
+ }
53
+
54
+ function findRow(rows: CustomizeRow[], label: string | RegExp): CustomizeRow {
55
+ const found = rows.find(row => { const text = typeof row.label === "function" ? row.label() : row.label; return typeof label === "string" ? text.startsWith(label) : label.test(text); });
56
+ assert.ok(found, `missing row ${label}`);
57
+ return found!;
58
+ }
59
+
60
+ /** `visible()` is optional; undefined means visible and a throwing callback fails closed. */
61
+ function isVisible(row: CustomizeRow): boolean {
62
+ try { return (row as CustomizeRow & { visible?: () => boolean }).visible?.() !== false; } catch { return false; }
63
+ }
64
+ function visibleRows(rows: CustomizeRow[]): CustomizeRow[] { return rows.filter(isVisible); }
65
+ function keyhintOf(row: CustomizeRow): string { return (row as CustomizeRow & { keyhint?: string }).keyhint ?? ""; }
66
+
67
+ function fakeInline(inputs: (string | undefined)[] = [], confirms: boolean[] = []): CustomizeInline {
68
+ return { input: async () => inputs.shift(), confirm: async () => confirms.shift() ?? false, disposed: false };
69
+ }
70
+ function label(row: CustomizeRow): string { return typeof row.label === "function" ? row.label() : row.label; }
71
+
72
+ test("Notifications rows toggle and mute directly without any nested native dialog", async () => {
73
+ const h = harness(); try {
74
+ const rows = h.rows();
75
+ await findRow(rows, "Audio notifications: off").action(fakeInline());
76
+ assert.equal(getNotificationService()!.getState().settings.enabled, true);
77
+ assert.equal(h.writes, 1);
78
+ await findRow(rows, "Audio: unmuted").action(fakeInline());
79
+ assert.equal(getNotificationService()!.getState().muted, true);
80
+ assert.equal(h.writes, 1, "mute persists in-process without writing configuration");
81
+ assert.deepEqual(h.nested, { select: 0, input: 0, confirm: 0 });
82
+ } finally { h.owner.retire(); }
83
+ });
84
+
85
+ test("labels and previews never probe, play or write and the card exposes no availability row", () => {
86
+ const h = harness(); try {
87
+ const rows = h.rows();
88
+ for (const row of rows) { label(row); row.preview?.(); }
89
+ assert.equal(h.probes, 0); assert.equal(h.played, 0); assert.equal(h.writes, 0);
90
+ assert.equal(rows.some(row => label(row).startsWith("Audio availability:")), false, "availability is removed from the card");
91
+ assert.equal(h.probes, 0, "rendering every row still never probes the backend");
92
+ } finally { h.owner.retire(); }
93
+ });
94
+
95
+ test("event rows cycle independently and p tests the selected sound without writing", async () => {
96
+ const h = harness(); try {
97
+ const rows = h.rows();
98
+ const failed = findRow(rows, "agent.failed:");
99
+ assert.match(label(failed), /builtin:error/);
100
+ await failed.action(fakeInline());
101
+ assert.equal(getNotificationService()!.getState().settings.audio.events["agent.failed"], "builtin:attention");
102
+ assert.equal(getNotificationService()!.getState().settings.audio.events["agent.completed"], "builtin:success");
103
+ assert.equal(h.writes, 1);
104
+ assert.equal(failed.key!("p", fakeInline()), true);
105
+ await tick(); await tick();
106
+ assert.equal(h.played, 1);
107
+ assert.equal(h.writes, 1, "preview never writes");
108
+ assert.deepEqual(h.nested, { select: 0, input: 0, confirm: 0 });
109
+ } finally { h.owner.retire(); }
110
+ });
111
+
112
+ test("f opens an inline field; a valid WAV saves while invalid content never writes", async () => {
113
+ const h = harness(); try {
114
+ const rows = h.rows();
115
+ const validated: string[] = [];
116
+ getNotificationService()!.validateFile = async (_ctx, sound) => { validated.push(sound); return true; };
117
+ const event = findRow(rows, "agent.failed:");
118
+ const pending = event.key!("f", fakeInline(["/local/my sound.wav"]));
119
+ assert.ok(pending, "the f shortcut consumes the key through an async task");
120
+ await pending;
121
+ await tick(); await tick();
122
+ assert.deepEqual(validated, ["file:/local/my sound.wav"]);
123
+ assert.equal(getNotificationService()!.getState().settings.audio.events["agent.failed"], "file:/local/my sound.wav");
124
+ assert.equal(h.writes, 1);
125
+ assert.equal(h.nested.input, 0, "the row never opens a native input dialog");
126
+ } finally { h.owner.retire(); }
127
+
128
+ const invalid = harness(); try {
129
+ const rows = invalid.rows();
130
+ findRow(rows, "agent.failed:").key!("f", fakeInline([fileURLToPath(new URL("../package.json", import.meta.url))]));
131
+ await tick(); await tick();
132
+ assert.equal(invalid.writes, 0);
133
+ } finally { invalid.owner.retire(); }
134
+ });
135
+
136
+ test("cancelled file input and unsupported URL never validate or write", async () => {
137
+ for (const input of [undefined, "https://example.test/sound.wav"]) {
138
+ const h = harness(); try {
139
+ const rows = h.rows();
140
+ findRow(rows, "agent.failed:").key!("f", fakeInline([input]));
141
+ await tick(); await tick();
142
+ assert.equal(h.writes, 0);
143
+ } finally { h.owner.retire(); }
144
+ }
145
+ });
146
+
147
+ test("malformed configuration needs fresh strict consent per attempt; a failed write grants none", async () => {
148
+ for (const failWrite of [false, true]) {
149
+ const h = harness({ malformed: true, failWrite }); try {
150
+ const rows = h.rows();
151
+ const enable = findRow(rows, "Audio notifications:");
152
+ await enable.action(fakeInline([], [false]));
153
+ assert.equal(h.writes, 0, "cancel preserves the file and settings");
154
+ assert.equal(getNotificationService()!.getState().malformed, true);
155
+ await enable.action(fakeInline([], [true]));
156
+ assert.equal(h.writes, 1, `confirmed attempt must run once (failWrite=${failWrite})`);
157
+ if (failWrite) {
158
+ assert.equal(getNotificationService()!.getState().malformed, true);
159
+ await enable.action(fakeInline([], [false]));
160
+ assert.equal(h.writes, 1, "a failed write cannot reuse prior consent");
161
+ await enable.action(fakeInline([], [true]));
162
+ assert.equal(h.writes, 2);
163
+ }
164
+ } finally { h.owner.retire(); }
165
+ }
166
+ });
167
+
168
+ test("owner replacement during validation cannot persist a stale choice", async () => {
169
+ const h = harness(); try {
170
+ const rows = h.rows();
171
+ getNotificationService()!.validateFile = async () => { h.owner.attach(h.ctx); return true; };
172
+ findRow(rows, "agent.failed:").key!("f", fakeInline(["/local/sound.wav"]));
173
+ await tick(); await tick();
174
+ assert.equal(h.writes, 0);
175
+ } finally { h.owner.retire(); }
176
+ });
177
+
178
+ test("closing the card during validation cannot persist a choice", async () => {
179
+ const h = harness(); try {
180
+ const rows = h.rows();
181
+ const fake = fakeInline(["/local/sound.wav"]);
182
+ getNotificationService()!.validateFile = async () => { fake.disposed = true; return true; };
183
+ findRow(rows, "agent.failed:").key!("f", fake);
184
+ await tick(); await tick();
185
+ assert.equal(h.writes, 0);
186
+ } finally { h.owner.retire(); }
187
+ });
188
+
189
+ function customizeView(rows: CustomizeRow[], onClose: () => void = () => {}) {
190
+ return new VisualCustomizeView({ rows, theme: { fg: (_role: string, text: string) => text }, rowsAvailable: () => 24, requestRender: () => {}, onClose,
191
+ profiles: { list: () => [], save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
192
+ }
193
+
194
+ /** Navigates the controls pane until the given label prefix carries the selection marker. */
195
+ function selectControl(view: VisualCustomizeView, prefix: string): void {
196
+ view.render(76);
197
+ view.handleInput("\x1b[C");
198
+ for (let i = 0; i < 60; i++) {
199
+ if (view.render(76).join("\n").includes(`\u25b8 ${prefix}`)) return;
200
+ view.handleInput("\x1b[B");
201
+ }
202
+ assert.fail(`could not select a control labelled ${prefix}`);
203
+ }
204
+
205
+ /** Detailed event rows are folded by default: expand Advanced inside the same card, then select. */
206
+ async function expandAndSelect(view: VisualCustomizeView, prefix: string): Promise<void> {
207
+ selectControl(view, "Advanced");
208
+ view.handleInput("\r");
209
+ await tick();
210
+ selectControl(view, prefix);
211
+ }
212
+
213
+ test("an in-flight local sound validation keeps busy, blocking a second field and any write until it settles", async () => {
214
+ const h = harness(); try {
215
+ const rows = h.rows();
216
+ let resolveValidation!: (value: boolean) => void;
217
+ const validated: string[] = [];
218
+ getNotificationService()!.validateFile = (_ctx, sound) => { validated.push(sound); return new Promise<boolean>(resolve => { resolveValidation = resolve; }); };
219
+ const view = customizeView(rows);
220
+ await expandAndSelect(view, "agent.failed:");
221
+ assert.match(view.render(160).join("\n"), /agent\.failed:/);
222
+ view.handleInput("f");
223
+ assert.match(view.render(160).join("\n"), /Local audio path/);
224
+ view.handleInput(WAV_FIXTURE);
225
+ view.handleInput("\r");
226
+ await tick();
227
+ assert.deepEqual(validated, [`file:${WAV_FIXTURE}`], "the submitted value is validated exactly once");
228
+ // Validation is still pending: `f` must not open a second field and Enter must not write.
229
+ view.handleInput("f");
230
+ view.handleInput("\r");
231
+ await tick();
232
+ assert.doesNotMatch(view.render(160).join("\n"), /Local audio path/, "a second f cannot open another field while busy");
233
+ assert.equal(h.writes, 0, "no write before the deferred validation resolves");
234
+ // Resolve: a single write, then busy releases for the next edit.
235
+ resolveValidation(true);
236
+ await tick(); await tick();
237
+ assert.equal(h.writes, 1, "the resolved validation writes exactly once");
238
+ assert.equal(getNotificationService()!.getState().settings.audio.events["agent.failed"], `file:${WAV_FIXTURE}`);
239
+ view.handleInput("f");
240
+ assert.match(view.render(160).join("\n"), /Local audio path/, "busy is released so the next edit opens a fresh field");
241
+ } finally { h.owner.retire(); }
242
+ });
243
+
244
+ test("Escape cancels the inline field while the async key task is busy and releases it", async () => {
245
+ const h = harness(); try {
246
+ const rows = h.rows();
247
+ getNotificationService()!.validateFile = async () => { throw new Error("validation must not run after a cancelled field"); };
248
+ const view = customizeView(rows);
249
+ await expandAndSelect(view, "agent.failed:");
250
+ view.handleInput("f");
251
+ assert.match(view.render(160).join("\n"), /Local audio path/);
252
+ view.handleInput("\x1b");
253
+ await tick(); await tick();
254
+ assert.doesNotMatch(view.render(160).join("\n"), /Local audio path/, "Escape cancels the inline field");
255
+ assert.equal(h.writes, 0, "a cancelled field never validates or writes");
256
+ view.handleInput("f");
257
+ assert.match(view.render(160).join("\n"), /Local audio path/, "busy is released after the cancelled key task");
258
+ } finally { h.owner.retire(); }
259
+ });
260
+
261
+ test("disposing the card while validation is in flight never writes the stale choice", async () => {
262
+ const h = harness(); try {
263
+ const rows = h.rows();
264
+ let resolveValidation!: (value: boolean) => void;
265
+ getNotificationService()!.validateFile = () => new Promise<boolean>(resolve => { resolveValidation = resolve; });
266
+ const view = customizeView(rows);
267
+ await expandAndSelect(view, "agent.failed:");
268
+ view.handleInput("f");
269
+ view.render(160);
270
+ view.handleInput(WAV_FIXTURE);
271
+ view.handleInput("\r");
272
+ await tick();
273
+ view.dispose();
274
+ resolveValidation(true);
275
+ await tick(); await tick();
276
+ assert.equal(h.writes, 0, "a disposed card must not persist the validated choice");
277
+ } finally { h.owner.retire(); }
278
+ });
279
+
280
+ test("missing owner yields unavailable labels and no writes or nested dialogs", async () => {
281
+ const h = harness(); try {
282
+ h.owner.retire();
283
+ const rows = h.rows();
284
+ assert.match(label(rows[0]!), /unavailable/);
285
+ await findRow(rows, "Audio notifications:").action(fakeInline());
286
+ assert.equal(h.writes, 0);
287
+ assert.deepEqual(h.nested, { select: 0, input: 0, confirm: 0 });
288
+ } finally { h.owner.retire(); }
289
+ });
290
+
291
+ test("event rows cover supported events and never expose session.shutdown", () => {
292
+ const h = harness(); try {
293
+ const labels = h.rows().map(label);
294
+ assert.ok(labels.some(value => value.startsWith("agent.failed:")));
295
+ assert.ok(labels.some(value => value.startsWith("subagent.timed_out:")));
296
+ assert.ok(labels.some(value => value.startsWith("session.started:")));
297
+ assert.ok(!labels.some(value => value.includes("session.shutdown")));
298
+ } finally { h.owner.retire(); }
299
+ });
300
+
301
+ test("p on every basic non-event notification control stays on the audio card, opens no profiles and never previews", () => {
302
+ const h = harness(); try {
303
+ const rows = h.rows();
304
+ const targets = ["Audio notifications:", "Audio: unmuted", "Advanced"];
305
+ const listed: number[] = [];
306
+ const view = new VisualCustomizeView({ rows, theme: { fg: (_role: string, text: string) => text }, rowsAvailable: () => 24, requestRender: () => {}, onClose: () => {}, profiles: {
307
+ list: () => { listed.push(1); return []; }, save: () => {}, apply: () => {}, delete: () => {}, reset: () => {},
308
+ } });
309
+ for (const [i, target] of targets.entries()) {
310
+ selectControl(view, target);
311
+ assert.ok(view.render(76).join("\n").includes(`\u25b8 ${target}`), `non-event row ${i} (${target}) must be the selected control`);
312
+ view.handleInput("p");
313
+ const frame = view.render(76).join("\n");
314
+ assert.equal(view.title(), "Audio notifications", `non-event row ${i} must keep the audio header`);
315
+ assert.match(frame, /Audio notifications/, `non-event row ${i} must stay on the audio card`);
316
+ assert.doesNotMatch(frame, /Visual profiles/, `non-event row ${i} must not open the profile pane`);
317
+ assert.equal(h.played, 0, `non-event row ${i} must not autoplay audio`);
318
+ }
319
+ assert.deepEqual(listed, [], "the profile catalog must never be listed from Notifications controls");
320
+ } finally { h.owner.retire(); }
321
+ });
322
+
323
+ test("the basic card exposes exactly six human controls and an Advanced toggle without raw ids", () => {
324
+ const h = harness(); try {
325
+ const labels = visibleRows(h.rows()).map(label);
326
+ assert.equal(labels.length, 6, "the basic card must expose exactly six controls");
327
+ assert.ok(labels.some(text => text.startsWith("Audio notifications:")), "the global switch row is required");
328
+ assert.ok(labels.some(text => /^Audio: (?:un)?muted/.test(text)), "the process mute row is required");
329
+ assert.ok(labels.some(text => text.startsWith("Success:")), "a Success group row is required");
330
+ assert.ok(labels.some(text => text.startsWith("Error:")), "an Error group row is required");
331
+ assert.ok(labels.some(text => text.startsWith("Attention:")), "an Attention group row is required");
332
+ assert.ok(labels.some(text => /^Advanced/.test(text)), "an Advanced toggle row is required");
333
+ assert.equal(labels.some(text => text.startsWith("Audio availability:")), false, "the availability row is removed");
334
+ assert.equal(labels.some(text => text.startsWith("Audio: restore preset")), false, "the restore row is removed");
335
+ for (const text of labels) {
336
+ assert.doesNotMatch(text, /builtin:/, `a basic label leaks a builtin id: ${text}`);
337
+ assert.doesNotMatch(text, /(?:agent|subagent|session)\./, `a basic label leaks a raw event id: ${text}`);
338
+ }
339
+ } finally { h.owner.retire(); }
340
+ });
341
+
342
+ test("context.high is exposed as an advanced per-event row that cycles independently", async () => {
343
+ const h = harness(); try {
344
+ const rows = h.rows();
345
+ assert.equal(label(findRow(rows, "context.high:")), "context.high: builtin:attention", "the attention tone is the context default");
346
+ await findRow(rows, /^Advanced/).action(fakeInline());
347
+ assert.equal(visibleRows(rows).some(row => label(row).startsWith("context.high:")), true);
348
+ await findRow(rows, "context.high:").action(fakeInline());
349
+ const events = getNotificationService()!.getState().settings.audio.events;
350
+ assert.equal(events["context.high"], null, "Enter cycles the context tone to silence without touching other events");
351
+ assert.equal(events["agent.attention"], "builtin:attention");
352
+ assert.equal(h.writes, 1);
353
+ } finally { h.owner.retire(); }
354
+ });
355
+
356
+ test("default groups read as their unambiguous human tone", () => {
357
+ const h = harness(); try {
358
+ const rows = h.rows();
359
+ assert.equal(label(findRow(rows, "Success:")), "Success: success tone");
360
+ assert.equal(label(findRow(rows, "Error:")), "Error: error tone");
361
+ assert.equal(label(findRow(rows, "Attention:")), "Attention: attention tone");
362
+ } finally { h.owner.retire(); }
363
+ });
364
+
365
+ test("Advanced folds the fourteen detailed event rows inside the same card without writes, probes or playback", async () => {
366
+ const h = harness(); try {
367
+ const rows = h.rows();
368
+ const detail = () => visibleRows(rows).map(label).filter(text => /^(?:agent|subagent|session|context)\./.test(text));
369
+ assert.equal(detail().length, 0, "detailed event rows stay folded by default");
370
+ const toggle = findRow(rows, /^Advanced/);
371
+ await toggle.action(fakeInline());
372
+ assert.equal(detail().length, 14, "expanding reveals the fourteen detailed rows");
373
+ await toggle.action(fakeInline());
374
+ assert.equal(detail().length, 0, "collapsing folds them again");
375
+ assert.equal(h.writes, 0); assert.equal(h.probes, 0); assert.equal(h.played, 0);
376
+ } finally { h.owner.retire(); }
377
+ });
378
+
379
+ test("global controls and the fold toggle advertise no sound key while group and detail rows do", async () => {
380
+ const h = harness(); try {
381
+ const rows = h.rows();
382
+ for (const prefix of ["Audio notifications:", "Audio: unmuted", "Advanced"])
383
+ assert.doesNotMatch(keyhintOf(findRow(rows, prefix)), /f sound/, `${prefix} is not a per-type file target`);
384
+ for (const { prefix } of GROUPS) assert.match(keyhintOf(findRow(rows, prefix)), /f sound/, `${prefix} assigns its own sound`);
385
+ await findRow(rows, /^Advanced/).action(fakeInline());
386
+ for (const prefix of ["agent.failed:", "agent.completed:"]) assert.match(keyhintOf(findRow(rows, prefix)), /f sound/);
387
+ } finally { h.owner.retire(); }
388
+ });
389
+
390
+ test("a group edit writes exactly its target events and preserves every other override", async () => {
391
+ const h = harness(); try {
392
+ const rows = h.rows();
393
+ await findRow(rows, "Success:").action(fakeInline());
394
+ const events = getNotificationService()!.getState().settings.audio.events;
395
+ assert.equal(events["agent.completed"], "builtin:error");
396
+ assert.equal(events["subagent.completed"], "builtin:error");
397
+ assert.equal(events["agent.failed"], "builtin:error", "error targets stay untouched");
398
+ assert.equal(events["agent.attention"], "builtin:attention", "attention target stays untouched");
399
+ assert.equal(events["subagent.waiting"], null, "unrelated events keep their value");
400
+ assert.equal(h.writes, 1);
401
+ } finally { h.owner.retire(); }
402
+ });
403
+
404
+ test("error and attention groups target verified events only and never add subagent.waiting", async () => {
405
+ const h = harness({ events: { "agent.failed": "builtin:error", "subagent.failed": "builtin:error", "subagent.timed_out": "builtin:error", "agent.attention": "builtin:attention", "subagent.waiting": "builtin:error" } }); try {
406
+ const rows = h.rows();
407
+ await findRow(rows, "Error:").action(fakeInline());
408
+ let events = getNotificationService()!.getState().settings.audio.events;
409
+ assert.equal(events["agent.failed"], "builtin:attention");
410
+ assert.equal(events["subagent.failed"], "builtin:attention");
411
+ assert.equal(events["subagent.timed_out"], "builtin:attention");
412
+ await findRow(rows, "Attention:").action(fakeInline());
413
+ events = getNotificationService()!.getState().settings.audio.events;
414
+ assert.equal(events["agent.attention"], null);
415
+ assert.equal(events["subagent.waiting"], "builtin:error", "attention never silently includes subagent.waiting");
416
+ } finally { h.owner.retire(); }
417
+ });
418
+
419
+ test("a group whose targets diverge reads Custom until an explicit Enter applies the recommended tone", async () => {
420
+ const h = harness({ events: { "agent.completed": "builtin:success", "subagent.completed": null } }); try {
421
+ const rows = h.rows();
422
+ const success = findRow(rows, "Success:");
423
+ assert.equal(label(success), "Success: Custom (varies)");
424
+ assert.equal(h.writes, 0);
425
+ await success.action(fakeInline());
426
+ const events = getNotificationService()!.getState().settings.audio.events;
427
+ assert.equal(events["agent.completed"], "builtin:success");
428
+ assert.equal(events["subagent.completed"], "builtin:success");
429
+ assert.equal(h.writes, 1);
430
+ } finally { h.owner.retire(); }
431
+ });
432
+
433
+ test("an omitted group reads silence and its edit writes only its targets without merging the preset", async () => {
434
+ const h = harness({ events: {} }); try {
435
+ const rows = h.rows();
436
+ const success = findRow(rows, "Success:");
437
+ assert.equal(label(success), "Success: silence");
438
+ await success.action(fakeInline());
439
+ const events = getNotificationService()!.getState().settings.audio.events;
440
+ assert.equal(events["agent.completed"], "builtin:success");
441
+ assert.equal(events["subagent.completed"], "builtin:success");
442
+ assert.equal(events["agent.failed"], undefined, "no preset merge adds untouched events");
443
+ assert.equal(events["agent.attention"], undefined);
444
+ } finally { h.owner.retire(); }
445
+ });
446
+
447
+ test("group p previews an unambiguous tone and mixed groups only point at Advanced without playback or writes", async () => {
448
+ const h = harness(); try {
449
+ const success = findRow(h.rows(), "Success:");
450
+ assert.ok(success.key, "group rows handle their own p shortcut");
451
+ assert.ok(await Promise.resolve(success.key!("p", fakeInline())), "p is consumed");
452
+ await tick(); await tick();
453
+ assert.equal(h.played, 1, "an unambiguous group previews its tone");
454
+ assert.equal(h.writes, 0);
455
+ } finally { h.owner.retire(); }
456
+ const mixed = harness({ events: { "agent.completed": "builtin:success", "subagent.completed": null } }); try {
457
+ const rows = mixed.rows();
458
+ const success = findRow(rows, "Success:");
459
+ assert.equal(label(success), "Success: Custom (varies)");
460
+ success.key!("p", fakeInline());
461
+ await tick(); await tick();
462
+ assert.equal(mixed.played, 0, "a mixed group never plays an arbitrary tone");
463
+ assert.equal(mixed.writes, 0);
464
+ assert.equal(label(success), "Success: Custom (varies)", "rendering and p never normalize the mixed state");
465
+ } finally { mixed.owner.retire(); }
466
+ });
467
+
468
+ test("Advanced expansion is ephemeral: rebuilding the card starts folded", async () => {
469
+ const h = harness(); try {
470
+ const first = h.rows();
471
+ await findRow(first, /^Advanced/).action(fakeInline());
472
+ assert.equal(visibleRows(first).map(label).filter(text => /^(?:agent|subagent|session)\./.test(text)).length, 13);
473
+ const second = h.rows();
474
+ assert.equal(visibleRows(second).map(label).filter(text => /^(?:agent|subagent|session)\./.test(text)).length, 0, "reopening folds the detailed rows");
475
+ } finally { h.owner.retire(); }
476
+ });
477
+
478
+ test("collapsed detailed rows are unreachable and never open the WAV field through the shared card", async () => {
479
+ const h = harness(); try {
480
+ const rows = h.rows();
481
+ let resolved = 0;
482
+ getNotificationService()!.validateFile = async () => { resolved++; return true; };
483
+ const view = customizeView(rows);
484
+ view.render(76);
485
+ view.handleInput("\x1b[C");
486
+ for (let i = 0; i < 40; i++) {
487
+ assert.doesNotMatch(view.render(76).join("\n"), /agent\.failed:/, "a folded event row must not be reachable");
488
+ view.handleInput("f");
489
+ view.handleInput("\x1b[B");
490
+ }
491
+ await tick(); await tick();
492
+ assert.equal(resolved, 0, "f on folded rows never opens the WAV field");
493
+ assert.equal(h.writes, 0);
494
+ } finally { h.owner.retire(); }
495
+ });
496
+
497
+ for (const { prefix, fixture, targets } of GROUPS) {
498
+ test(`${prefix} f assigns one validated WAV to all its targets in one atomic write`, async () => {
499
+ const h = harness(); try {
500
+ const rows = h.rows();
501
+ const validated: string[] = [];
502
+ getNotificationService()!.validateFile = async (_ctx, sound) => { validated.push(sound); return true; };
503
+ const before = structuredClone(getNotificationService()!.getState().settings.audio.events);
504
+ const pending = findRow(rows, prefix).key!("f", fakeInline([fixture]));
505
+ assert.ok(pending, "the group f consumes the key as an async task");
506
+ await Promise.resolve(pending);
507
+ await tick(); await tick();
508
+ assert.deepEqual(validated, [`file:${fixture}`], "the file is validated exactly once");
509
+ const events = getNotificationService()!.getState().settings.audio.events;
510
+ for (const target of targets) assert.equal(events[target as NotificationEvent], `file:${fixture}`, `${target} must take the group WAV`);
511
+ for (const [key, value] of Object.entries(before)) {
512
+ if ((targets as readonly string[]).includes(key)) continue;
513
+ assert.deepEqual(events[key as NotificationEvent], value, `${key} must stay untouched`);
514
+ }
515
+ assert.equal(h.writes, 1, "one atomic write");
516
+ assert.equal(getNotificationService()!.getState().settings.enabled, false, "enabled is preserved");
517
+ } finally { h.owner.retire(); }
518
+ });
519
+ }
520
+
521
+ test("group f keeps configuration silent when the file is invalid, cancelled, owner replaced or the card disposed", async () => {
522
+ for (const mode of ["invalid", "cancel", "owner", "dispose"] as const) {
523
+ const h = harness(); try {
524
+ const rows = h.rows();
525
+ const success = findRow(rows, "Success:");
526
+ const inline: CustomizeInline = fakeInline([mode === "cancel" ? undefined : WAV_FIXTURES.success]);
527
+ getNotificationService()!.validateFile = async () => {
528
+ if (mode === "owner") h.owner.attach(h.ctx);
529
+ if (mode === "dispose") inline.disposed = true;
530
+ return mode !== "invalid";
531
+ };
532
+ await Promise.resolve(success.key!("f", inline));
533
+ await tick(); await tick();
534
+ assert.equal(h.writes, 0, `mode=${mode} must never write configuration`);
535
+ } finally { h.owner.retire(); }
536
+ }
537
+ });
538
+
539
+ test("a uniform file group shows the basename, prefills its path and previews that exact file", async () => {
540
+ const file: `file:${string}` = `file:${WAV_FIXTURES.success}`;
541
+ const h = harness({ events: { "agent.completed": file, "subagent.completed": file } }); try {
542
+ const rows = h.rows();
543
+ const success = findRow(rows, "Success:");
544
+ assert.equal(label(success), `Success: ${basename(WAV_FIXTURES.success)}`);
545
+ assert.doesNotMatch(label(success), /builtin:|file:|agent\.|subagent\./, "the main label stays human and raw-reference free");
546
+ getNotificationService()!.validateFile = async () => true;
547
+ const requests: string[] = [];
548
+ const inline: CustomizeInline = { input: async request => { requests.push(request.value); return WAV_FIXTURES.success; }, confirm: async () => false, disposed: false };
549
+ await Promise.resolve(success.key!("f", inline));
550
+ await tick(); await tick();
551
+ assert.deepEqual(requests, [WAV_FIXTURES.success], "the field is prefilled with the current file path");
552
+ assert.equal(success.key!("p", fakeInline()), true);
553
+ await tick(); await tick();
554
+ assert.equal(h.played, 1, "p previews the group's exact file");
555
+ assert.equal(h.writes, 1, "assigning the current file still validates and writes once");
556
+ } finally { h.owner.retire(); }
557
+ });
558
+
559
+ test("a mixed group f overrides all its targets with one file and preserves other groups", async () => {
560
+ const h = harness({ events: { "agent.completed": "builtin:success", "subagent.completed": null, "agent.failed": "builtin:error", "agent.attention": "builtin:attention" } }); try {
561
+ const rows = h.rows();
562
+ getNotificationService()!.validateFile = async () => true;
563
+ await Promise.resolve(findRow(rows, "Success:").key!("f", fakeInline([WAV_FIXTURES.attention])));
564
+ await tick(); await tick();
565
+ const events = getNotificationService()!.getState().settings.audio.events;
566
+ assert.equal(events["agent.completed"], `file:${WAV_FIXTURES.attention}`);
567
+ assert.equal(events["subagent.completed"], `file:${WAV_FIXTURES.attention}`);
568
+ assert.equal(events["agent.failed"], "builtin:error", "other groups stay stable");
569
+ assert.equal(events["agent.attention"], "builtin:attention");
570
+ assert.equal(h.writes, 1);
571
+ } finally { h.owner.retire(); }
572
+ });
573
+
574
+ test("group targets never intersect unsupported siblings such as subagent.waiting", async () => {
575
+ const h = harness({ events: { "agent.attention": "builtin:attention", "subagent.waiting": "builtin:error" } }); try {
576
+ const rows = h.rows();
577
+ getNotificationService()!.validateFile = async () => true;
578
+ await Promise.resolve(findRow(rows, "Attention:").key!("f", fakeInline([WAV_FIXTURES.attention])));
579
+ await tick(); await tick();
580
+ const events = getNotificationService()!.getState().settings.audio.events;
581
+ assert.equal(events["agent.attention"], `file:${WAV_FIXTURES.attention}`);
582
+ assert.equal(events["subagent.waiting"], "builtin:error", "the attention group never intersects subagent.waiting");
583
+ } finally { h.owner.retire(); }
584
+ });
585
+
586
+ test("the rendered basic card shows three independent group choices and hides per-event rows until Advanced", async () => {
587
+ const h = harness(); try {
588
+ const rows = h.rows();
589
+ const view = customizeView(rows);
590
+ view.render(76);
591
+ view.handleInput("\x1b[C");
592
+ const frame = () => view.render(76).join("\n");
593
+ for (const { prefix } of GROUPS) assert.match(frame(), new RegExp(prefix), `${prefix} must be visible on the basic card`);
594
+ assert.doesNotMatch(frame(), /agent\.failed:/, "no per-event technical row is visible before Advanced");
595
+ selectControl(view, "Success:");
596
+ assert.match(frame(), /f sound/, "the selected group advertises its own file key");
597
+ selectControl(view, "Advanced:");
598
+ view.handleInput("\r");
599
+ await tick();
600
+ assert.match(frame(), /agent\.failed:/, "Advanced reveals the per-event exceptions");
601
+ } finally { h.owner.retire(); }
602
+ });
603
+
604
+ for (const { prefix, fixture, targets } of GROUPS) {
605
+ test(`the selected ${prefix.slice(0, -1)} basic preview is human with no builtin:, file: refs or raw event ids`, async () => {
606
+ const builtin = harness(); try {
607
+ const view = customizeView(builtin.rows());
608
+ selectControl(view, prefix);
609
+ const frame = view.render(90).join("\n");
610
+ assert.match(frame, new RegExp(`${prefix} ${GROUP_TONE[prefix]}`), "the friendly tone label must be visible in the body");
611
+ assert.doesNotMatch(frame, /builtin:/, "the basic body/preview must not leak a builtin id");
612
+ assert.doesNotMatch(frame, /file:/, "the basic body/preview must not leak a raw file ref");
613
+ assert.doesNotMatch(frame, /(?:agent|subagent|session)\./, "the basic body/preview must not leak a raw event id");
614
+ } finally { builtin.owner.retire(); }
615
+
616
+ const own = harness({ events: groupEvents(targets, `file:${fixture}`) }); try {
617
+ const name = basename(fixture);
618
+ const view = customizeView(own.rows());
619
+ selectControl(view, prefix);
620
+ const frame = view.render(90).join("\n");
621
+ assert.match(frame, new RegExp(`${prefix} ${name}`), "a uniform file group shows the friendly basename");
622
+ assert.doesNotMatch(frame, /file:/, "the basic preview must not leak the file: ref");
623
+ assert.doesNotMatch(frame, new RegExp(fixture.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), "the absolute path must not appear");
624
+ assert.doesNotMatch(frame, /builtin:/);
625
+ assert.doesNotMatch(frame, /(?:agent|subagent|session)\./);
626
+ } finally { own.owner.retire(); }
627
+ });
628
+ }
629
+
630
+ test("a mixed basic group preview reads Custom (varies) without raw references", async () => {
631
+ const h = harness({ events: { "agent.completed": "builtin:success", "subagent.completed": null } }); try {
632
+ const view = customizeView(h.rows());
633
+ selectControl(view, "Success:");
634
+ const frame = view.render(90).join("\n");
635
+ assert.match(frame, /Success: Custom \(varies\)/, "the mixed label must be friendly");
636
+ assert.doesNotMatch(frame, /builtin:/, "mixed preview must not pick or leak a builtin");
637
+ assert.doesNotMatch(frame, /file:/, "mixed preview must not leak a raw file ref");
638
+ assert.doesNotMatch(frame, /(?:agent|subagent|session)\./, "mixed preview must not leak raw event ids");
639
+ } finally { h.owner.retire(); }
640
+ });
641
+
642
+ test("the Advanced per-event rows intentionally keep raw ids and builtin refs", async () => {
643
+ const h = harness(); try {
644
+ const rows = h.rows();
645
+ const view = customizeView(rows);
646
+ view.render(76);
647
+ view.handleInput("\x1b[C");
648
+ selectControl(view, "Advanced:");
649
+ view.handleInput("\r");
650
+ await tick();
651
+ selectControl(view, "agent.failed:");
652
+ const frame = view.render(90).join("\n");
653
+ assert.match(frame, /agent\.failed: builtin:error/, "advanced details intentionally show the raw event id and builtin ref");
654
+ } finally { h.owner.retire(); }
655
+ });