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
@@ -181,8 +181,6 @@ interface ParityRuntime {
181
181
  interface ParityRuntimeOptions {
182
182
  candidateViews?: CandidateViewRegistry;
183
183
  pendingReviewConsentRegistry?: PendingReviewConsentRegistry;
184
- now?: () => number;
185
- scheduleTimer?: (callback: () => void, delayMs: number) => { unref: () => void };
186
184
  }
187
185
 
188
186
  function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityRuntimeOptions = {}): ParityRuntime {
@@ -193,8 +191,6 @@ function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityR
193
191
  nativeReviewCli,
194
192
  candidateViews: options.candidateViews ?? new CandidateViewRegistry(),
195
193
  pendingReviewConsentRegistry: options.pendingReviewConsentRegistry ?? new PendingReviewConsentRegistry(),
196
- now: options.now,
197
- scheduleTimer: options.scheduleTimer,
198
194
  } as unknown as Parameters<typeof createGentleAiExtension>[0];
199
195
  __testing.createGentleAiExtension(dependencies)({
200
196
  on(name: string, handler: RegisteredEvent) { events.set(name, handler); },
@@ -330,7 +326,7 @@ async function answerConsent(runtime: ParityRuntime, cwd: string, binding: unkno
330
326
  // that names itself and its exit. It still carries the current negotiated
331
327
  // STATUS as reconciliation context, but it never reads as a healthy
332
328
  // pre-start "ready".
333
- function assertStaleConsentBinding(outcome: Record<string, unknown>, binding: unknown, code: "consent-binding-unknown" | "consent-binding-expired" | "consent-binding-already-consumed"): void {
329
+ function assertStaleConsentBinding(outcome: Record<string, unknown>, binding: unknown, code: "consent-binding-unknown" | "consent-binding-already-consumed"): void {
334
330
  assert.equal(outcome.status, "blocked");
335
331
  assert.equal(outcome.outcome, "consent-binding-stale");
336
332
  assert.equal(outcome.consent_binding, binding);
@@ -677,7 +673,7 @@ test("unavailable and ambiguous consent follow-ups never replay a consumed bindi
677
673
  assert.equal(statusCalls, callsBeforeStaleReconciliation + 1, "already-consumed binding reconciliation performs exactly one additional STATUS call");
678
674
  });
679
675
 
680
- test("concurrent answers atomically claim consent before review-mode gating and ignore a queued expiry callback", async (t) => {
676
+ test("concurrent answers atomically claim consent before review-mode gating", async (t) => {
681
677
  const cwd = repository(t);
682
678
  const fixture = consentNative(cwd);
683
679
  const candidateViews = new CandidateViewRegistry();
@@ -687,7 +683,6 @@ test("concurrent answers atomically claim consent before review-mode gating and
687
683
  cleanupCalls += 1;
688
684
  cleanup(token);
689
685
  };
690
- const scheduled: Array<() => void> = [];
691
686
  let releaseModeGate: (() => void) | undefined;
692
687
  const modeGate = new Promise<void>((resolve) => { releaseModeGate = resolve; });
693
688
  let modeCalls = 0;
@@ -702,13 +697,12 @@ test("concurrent answers atomically claim consent before review-mode gating and
702
697
  let nativeAnswerCalls = 0;
703
698
  fixture.native.answerConsent = async (request) => {
704
699
  nativeAnswerCalls += 1;
705
- assert.equal(cleanupCalls, 0, "a queued expiry callback must not clean claimed candidate authority before the provider answer");
700
+ assert.equal(cleanupCalls, 0, "candidate authority remains live before the provider answer");
706
701
  await providerGate;
707
702
  return await nativeAnswer(request);
708
703
  };
709
704
  const runtime = parityRuntime(fixture.native, {
710
705
  candidateViews,
711
- scheduleTimer: (callback) => { scheduled.push(callback); return { unref() {} }; },
712
706
  });
713
707
  const blocked = await beginConsent(runtime, cwd);
714
708
  fixture.native.reviewMode = async () => {
@@ -726,9 +720,7 @@ test("concurrent answers atomically claim consent before review-mode gating and
726
720
  assert.equal(modeCalls, 1, "the first answer pauses at the asynchronous review-mode gate");
727
721
  const second = answerConsent(runtime, cwd, blocked.consent_binding, "declined");
728
722
  await new Promise<void>((resolve) => setImmediate(resolve));
729
- assert.equal(scheduled.length, 1);
730
- scheduled[0]!();
731
- assert.equal(cleanupCalls, 0, "a queued expiry callback is a harmless no-op after the answer claim");
723
+ assert.equal(cleanupCalls, 0, "claimed consent retains candidate authority until the answer settles");
732
724
 
733
725
  releaseModeGate!();
734
726
  releaseProvider!();
@@ -741,47 +733,19 @@ test("concurrent answers atomically claim consent before review-mode gating and
741
733
  assert.equal(cleanupCalls, 1, "only the answered binding cleans candidate authority");
742
734
  });
743
735
 
744
- test("timer-cleaned and synchronously pruned consent bindings remain expired within their session", async (t) => {
736
+ test("unanswered consent remains reusable after the former deadline and answers only once", async (t) => {
745
737
  const cwd = repository(t);
746
738
  const fixture = consentNative(cwd);
747
- const registry = new PendingReviewConsentRegistry();
748
- const start = 1_000;
749
- let now = start;
750
- const scheduled: Array<() => void> = [];
751
- const runtime = parityRuntime(fixture.native, {
752
- pendingReviewConsentRegistry: registry,
753
- now: () => now,
754
- scheduleTimer: (callback) => { scheduled.push(callback); return { unref() {} }; },
755
- });
739
+ const runtime = parityRuntime(fixture.native);
740
+ let now = 1_000;
741
+ t.mock.method(Date, "now", () => now);
756
742
  const first = await beginConsent(runtime, cwd);
743
+ now += 24 * 60 * 60 * 1000;
757
744
  const reused = await beginConsent(runtime, cwd);
758
745
  assert.equal(reused.consent_binding, first.consent_binding);
759
- assert.equal(scheduled.length, 1);
760
- now += 10 * 60 * 1000;
761
- // The real TTL callback drops the frozen candidate immediately. Its
762
- // session-local disposition remains only to classify this stale answer.
763
- scheduled[0]!();
764
- const timerExpired = await answerConsent(runtime, cwd, first.consent_binding, "declined");
765
- assert.equal(timerExpired.operation, "answer-consent");
766
- assertStaleConsentBinding(timerExpired, first.consent_binding, "consent-binding-expired");
767
- assert.match(String((timerExpired.diagnostics as { message?: unknown }).message), /expired after 10 minutes/);
768
- const second = await beginConsent(runtime, cwd);
769
- assert.notEqual(second.consent_binding, first.consent_binding);
770
- assert.equal(scheduled.length, 2);
771
- assertStaleConsentBinding(await answerConsent(runtime, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-expired");
772
-
773
- // No second callback runs: START synchronously prunes this unused binding
774
- // before it can reuse the frozen candidate, and its answer stays typed expiry.
775
- now += 10 * 60 * 1000;
776
- const third = await beginConsent(runtime, cwd);
777
- assert.notEqual(third.consent_binding, second.consent_binding);
778
- assertStaleConsentBinding(await answerConsent(runtime, cwd, second.consent_binding, "declined"), second.consent_binding, "consent-binding-expired");
779
-
780
- const reloaded = parityRuntime(fixture.native, { pendingReviewConsentRegistry: new PendingReviewConsentRegistry() });
781
- assertStaleConsentBinding(await answerConsent(reloaded, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-unknown");
782
- const afterReload = await beginConsent(reloaded, cwd);
783
- assert.notEqual(afterReload.consent_binding, third.consent_binding);
784
- assert.equal(fixture.starts.count, 5);
746
+ const answered = await answerConsent(runtime, cwd, first.consent_binding, "declined");
747
+ assert.equal(answered.outcome, "consent-declined-this-candidate");
748
+ assertStaleConsentBinding(await answerConsent(runtime, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-already-consumed");
785
749
  });
786
750
 
787
751
  test("native START maps only provider facts and omits absent evidence", async (t) => {
@@ -89,3 +89,24 @@ test("an empty file is named before it is described", () => {
89
89
  test("a reason with no path contributes no phrase", () => {
90
90
  assert.deepEqual(nativeRiskEvidencePhrases("high", [{ code: "empty_content" }]), []);
91
91
  });
92
+
93
+ // Field truth from gentle-ai main (4.1.0-dev): one committed high-risk
94
+ // candidate's consent/v3 `risk_evidence` beside its START/v4 `risk_reasons`.
95
+ test("dangerous sink and agent escalation reasons render the gentle-ai phrases", () => {
96
+ assert.deepEqual(
97
+ nativeRiskEvidencePhrases("high", [
98
+ { code: "dangerous_sink", signal: "dangerous_sink", path: "auth/session.py" },
99
+ { code: "hot_path", signal: "auth", path: "auth/session.py" },
100
+ { code: "process_boundary", signal: "shell_process", path: "auth/session.py" },
101
+ ]),
102
+ [
103
+ "a dangerous code pattern in auth/session.py",
104
+ "authentication in auth/session.py",
105
+ "code that starts other processes in auth/session.py",
106
+ ],
107
+ );
108
+ assert.deepEqual(
109
+ nativeRiskEvidencePhrases("high", [{ code: "agent_escalation", signal: "agent_escalation" }]),
110
+ ["the agent that made this change flagged it as high risk"],
111
+ );
112
+ });
@@ -0,0 +1,87 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ // gentle-shell#1731 T13 (evidence L24): in bench task x4 the user wrote "I may
7
+ // stop you between the two and pick the second part up later"; every Gentle
8
+ // cell delivered only the first part and paused by itself. Hedged delivery
9
+ // wording asks for resumable notes and separate commits, never a stop, and
10
+ // the close step must not leave tracked tasks pending without a user stop.
11
+
12
+ const REPO_ROOT = join(import.meta.dirname, "..");
13
+ const read = (relative: string): string => readFileSync(join(REPO_ROOT, relative), "utf8");
14
+
15
+ const tracking = read("assets/orchestrator-tracking.md");
16
+ const core = read("assets/orchestrator.md");
17
+
18
+ function authorizationSection(): string {
19
+ const start = tracking.indexOf("#### Authorization and progress");
20
+ assert.ok(start !== -1, "tracking module must keep its Authorization and progress section");
21
+ const next = tracking.indexOf("\n#### ", start + 1);
22
+ return tracking.slice(start, next === -1 ? undefined : next);
23
+ }
24
+
25
+ test("T13: conditional change intent is narrowed to whether a change is authorized at all", () => {
26
+ const section = authorizationSection();
27
+ assert.ok(
28
+ section.includes(
29
+ "Ambiguous or conditional change intent (unclear whether a change is authorized at all) requires one clarification; stop and wait.",
30
+ ),
31
+ "the clarification clause must be scoped to authorization itself",
32
+ );
33
+ assert.ok(
34
+ !section.includes("Ambiguous or conditional change intent requires one clarification"),
35
+ "the unscoped clarification clause must be gone",
36
+ );
37
+ });
38
+
39
+ test("T13: hedged delivery wording asks for resumable notes and separate commits, not a stop", () => {
40
+ const section = authorizationSection();
41
+ for (const clause of [
42
+ "Hedged delivery wording (the user may stop you, review commits one at a time, or pick the work up later)",
43
+ "asks for resumable notes and separate commits, not a stop",
44
+ "complete every authorized task in the current turn unless the user explicitly says to wait",
45
+ ]) {
46
+ assert.ok(section.includes(clause), `tracking module is missing: ${clause}`);
47
+ }
48
+ const hedge = section.slice(section.indexOf("Hedged delivery wording"));
49
+ const sentence = hedge.slice(0, hedge.indexOf(". ") + 1);
50
+ assert.doesNotMatch(sentence, /\bask (?:one|a|the user)\b|question/i, "the hedge rule must not add a product or design question");
51
+ });
52
+
53
+ test("T13: the Close gate requires continuing or quoting the user's explicit stop", () => {
54
+ const section = authorizationSection();
55
+ assert.ok(
56
+ section.includes(
57
+ "Close gate: before the final answer, if any tracked task in the feature document or `todo` is still pending, continue it or quote the exact user sentence that orders the stop.",
58
+ ),
59
+ "tracking module must carry the Close gate",
60
+ );
61
+ });
62
+
63
+ test("T13: budgets hold and the pinned human-control line is untouched", () => {
64
+ const bytes = Buffer.byteLength(tracking, "utf8");
65
+ assert.ok(bytes <= 12_600, `orchestrator-tracking.md is ${bytes} B, over its 12,600 B budget`);
66
+ assert.ok(core.includes("- Preserve human control: user decisions beat agent momentum."), "core human-control line must stay verbatim");
67
+ assert.ok(core.includes("4. **Track**") && core.includes("`orchestrator-tracking.md`"), "Track must still load the tracking module");
68
+ });
69
+
70
+ test("T13: the always-on harness narrows conditional intent and gates Close on pending tasks", () => {
71
+ // The harness ODD steps are injected on every request, including the small
72
+ // path that never loads the tracking module, so the narrowing must live there too.
73
+ const harness = read("extensions/gentle-ai.ts");
74
+ assert.ok(
75
+ harness.includes("Ambiguous or conditional change intent (unclear whether a change is authorized at all) gets one clarification; stop and wait."),
76
+ "harness Authorize step must narrow conditional intent",
77
+ );
78
+ assert.ok(!harness.includes("Ambiguous or conditional change intent gets one clarification"), "the broad harness wording must be gone");
79
+ assert.ok(
80
+ harness.includes("A user saying they may stop you or resume later asks for notes and separate commits, not a stop."),
81
+ "harness must say hedged delivery wording is not a stop",
82
+ );
83
+ assert.ok(
84
+ harness.includes("Never end with a tracked task pending unless you quote the user's explicit stop."),
85
+ "harness Close step must gate pending tracked tasks",
86
+ );
87
+ });
@@ -0,0 +1,281 @@
1
+ import assert from "node:assert/strict";
2
+ import { EventEmitter } from "node:events";
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { createServer, type Server } from "node:net";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { test } from "node:test";
8
+ import { NativePulsePlayer, NATIVE_PULSE_SCHEMA, type NativePulseOptions, type NativePulseSpawnOptions } from "../lib/notification-audio-native.ts";
9
+
10
+ // --- independent golden encoders (deliberately NOT the production codec) ---
11
+ const taggedU32 = (value: number): Buffer => { const b = Buffer.alloc(5); b[0] = 0x4c; b.writeUInt32BE(value, 1); return b; };
12
+ const taggedString = (value: string): Buffer => Buffer.concat([Buffer.from([0x74]), Buffer.from(value, "utf8"), Buffer.from([0x00])]);
13
+ const nullString = (): Buffer => Buffer.from([0x4e]);
14
+ const boolFalse = (): Buffer => Buffer.from([0x30]);
15
+ const usec = (value: number): Buffer => { const b = Buffer.alloc(9); b[0] = 0x55; b.writeBigUInt64BE(BigInt(value), 1); return b; };
16
+ const sampleSpec = (format: number, channels: number, rate: number): Buffer => { const b = Buffer.alloc(7); b[0] = 0x61; b[1] = format; b[2] = channels; b.writeUInt32BE(rate, 3); return b; };
17
+ const channelMap = (channels: number): Buffer => Buffer.from([0x6d, channels, ...(channels === 1 ? [0] : [1, 2])]);
18
+ const reply = (command: number, tag: number, ...args: Buffer[]): Buffer => Buffer.concat([taggedU32(command), taggedU32(tag), ...args]);
19
+ const frame = (channel: number, payload: Buffer): Buffer => { const header = Buffer.alloc(20); header.writeUInt32BE(payload.length, 0); header.writeUInt32BE(channel, 4); return Buffer.concat([header, payload]); };
20
+ const taggedValueAt = (payload: Buffer, at: number): number => payload.readUInt32BE(at + 1);
21
+
22
+ function wav(bits: number, channels: number, rate: number, frames: number): Buffer {
23
+ const sampleBytes = bits / 8; const dataSize = frames * channels * sampleBytes;
24
+ const b = Buffer.alloc(44 + dataSize, 0x11);
25
+ b.write("RIFF", 0); b.writeUInt32LE(36 + dataSize, 4); b.write("WAVE", 8);
26
+ b.write("fmt ", 12); b.writeUInt32LE(16, 16); b.writeUInt16LE(1, 20);
27
+ b.writeUInt16LE(channels, 22); b.writeUInt32LE(rate, 24); b.writeUInt32LE(rate * channels * sampleBytes, 28);
28
+ b.writeUInt16LE(channels * sampleBytes, 32); b.writeUInt16LE(bits, 34);
29
+ b.write("data", 36); b.writeUInt32LE(dataSize, 40);
30
+ return b;
31
+ }
32
+ function dataOf(bytes: Buffer): Buffer { let offset = 12; for (;;) { const id = bytes.toString("ascii", offset, offset + 4); const size = bytes.readUInt32LE(offset + 4); if (id === "data") return bytes.subarray(offset + 8, offset + 8 + size); offset += 8 + size + (size % 2); } }
33
+
34
+ interface FakeServer { readonly path: string; readonly seen: number[]; readonly pcm: Buffer[]; close(): Promise<void>; }
35
+ async function startFakeServer(): Promise<FakeServer> {
36
+ const dir = mkdtempSync(join(tmpdir(), "native-pulse-"));
37
+ const path = join(dir, "native");
38
+ const seen: number[] = []; const pcm: Buffer[] = [];
39
+ const server: Server = createServer(socket => {
40
+ let buffer = Buffer.alloc(0);
41
+ const write = (value: Buffer) => { if (!socket.destroyed) socket.write(value); };
42
+ const send = (payload: Buffer) => write(frame(0xffffffff, payload));
43
+ socket.on("error", () => { /* child closed */ });
44
+ socket.on("data", chunk => {
45
+ buffer = Buffer.concat([buffer, chunk]);
46
+ for (;;) {
47
+ if (buffer.length < 20) break;
48
+ const length = buffer.readUInt32BE(0);
49
+ if (buffer.length < 20 + length) break;
50
+ const payload = Buffer.from(buffer.subarray(20, 20 + length));
51
+ const channel = buffer.readUInt32BE(4);
52
+ buffer = buffer.subarray(20 + length);
53
+ if (channel !== 0xffffffff) { pcm.push(payload); continue; }
54
+ const command = taggedValueAt(payload, 0); const tag = taggedValueAt(payload, 5);
55
+ seen.push(command);
56
+ if (command === 8) { send(reply(2, tag, taggedU32(35))); continue; }
57
+ if (command === 9) { send(reply(2, tag, taggedU32(0))); continue; }
58
+ if (command === 20) { send(reply(2, tag, taggedString("pulseaudio"), taggedString("17.0"), taggedString("user"), taggedString("host"), sampleSpec(3, 1, 8000), taggedString("auto_null"), nullString(), taggedU32(1))); continue; }
59
+ if (command === 3) {
60
+ const spec = { format: payload[11]!, channels: payload[12]!, rate: payload.readUInt32BE(13) };
61
+ const dataLength = 0; // worker sends initial missing = 0 then requests; keep it simple
62
+ write(frame(0xffffffff, reply(2, tag, taggedU32(7), taggedU32(8), taggedU32(dataLength), taggedU32(0xffffffff), taggedU32(0), taggedU32(0), taggedU32(0), sampleSpec(spec.format, spec.channels, spec.rate), channelMap(spec.channels), taggedU32(0), taggedString("auto_null"), boolFalse(), usec(0))));
63
+ write(frame(0xffffffff, reply(61, 0xffffffff, taggedU32(7), taggedU32(64 * 1024))));
64
+ continue;
65
+ }
66
+ if (command === 12) { send(reply(2, tag)); continue; }
67
+ if (command === 4) { send(reply(2, tag)); continue; }
68
+ }
69
+ });
70
+ });
71
+ await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(path, () => resolve()); });
72
+ return { path, seen, pcm, close: async () => { await new Promise<void>(resolve => server.close(() => resolve())); rmSync(dir, { recursive: true, force: true }); } };
73
+ }
74
+
75
+ class FakeChild extends EventEmitter {
76
+ readonly stdout = new EventEmitter();
77
+ readonly stderr = new EventEmitter();
78
+ readonly kills: string[] = [];
79
+ kill(signal: string): boolean { this.kills.push(signal); return true; }
80
+ }
81
+ function recorder(platform = "linux", overrides: Partial<NativePulseOptions> = {}) {
82
+ const spawned: Array<{ executable: string; args: string[]; options: NativePulseSpawnOptions }> = [];
83
+ const children: FakeChild[] = [];
84
+ const player = new NativePulsePlayer({
85
+ platform,
86
+ spawn: (executable, args, options) => { spawned.push({ executable, args, options }); const child = new FakeChild(); children.push(child); return child; },
87
+ env: { PULSE_SERVER: "unix:/tmp/x", NODE_OPTIONS: "--inspect", DEBUG: "*", HOME: "/home/u" },
88
+ ...overrides,
89
+ });
90
+ return { player, spawned, children };
91
+ }
92
+ const okProbe = () => JSON.stringify({ schema: NATIVE_PULSE_SCHEMA, ok: true, available: true, formats: ["wav"] });
93
+
94
+ test("bridge is lazy and spawns owned Node with fixed flags, scrubbed env and Linux only", async () => {
95
+ const { player, spawned, children } = recorder();
96
+ assert.deepEqual(spawned, []);
97
+ const probing = player.probe();
98
+ assert.equal(spawned.length, 1);
99
+ const [call] = spawned;
100
+ assert.equal(call.executable, process.execPath);
101
+ assert.equal(call.args[0], "--experimental-strip-types");
102
+ assert.equal(call.args[1], "--max-old-space-size=32");
103
+ assert.match(call.args[2]!, /notification-pulse-worker\.ts$/);
104
+ assert.equal(call.args[3], "probe");
105
+ assert.deepEqual(call.options.stdio, ["ignore", "pipe", "pipe"]);
106
+ assert.equal(call.options.shell, false); assert.equal(call.options.windowsHide, true); assert.equal(call.options.detached, false);
107
+ const env = call.options.env as NodeJS.ProcessEnv;
108
+ assert.equal(env.NODE_NO_WARNINGS, "1"); assert.equal(env.GENTLE_PI_AGENTS_CHILD, "1");
109
+ assert.equal(env.NODE_OPTIONS, undefined); assert.equal(env.DEBUG, undefined); assert.equal(env.PULSE_SERVER, "unix:/tmp/x");
110
+ assert.deepEqual(player.getNativeFormats(), []); // default unavailable until a real probe
111
+ children[0]!.stdout.emit("data", okProbe());
112
+ children[0]!.emit("close");
113
+ assert.deepEqual(await probing, { available: true, formats: ["wav"] });
114
+ assert.deepEqual(player.getNativeFormats(), ["wav"]);
115
+ });
116
+
117
+ test("abort before spawn performs no work and unsupported platforms never spawn", async () => {
118
+ const { player, spawned } = recorder();
119
+ const controller = new AbortController(); controller.abort();
120
+ assert.deepEqual(await player.probe(controller.signal), { available: false, formats: [] });
121
+ assert.equal(spawned.length, 0);
122
+ for (const platform of ["darwin", "win32"]) {
123
+ const other = recorder(platform);
124
+ assert.equal(other.player.supportsTarget(), false);
125
+ assert.deepEqual(await other.player.probe(), { available: false, formats: [] });
126
+ await assert.rejects(other.player.play("/tmp/s.wav"), /Native pulse|unsupported/);
127
+ assert.equal(other.spawned.length, 0);
128
+ }
129
+ });
130
+
131
+ test("play gate false prevents spawning and no async runs after it", async () => {
132
+ const { player, spawned } = recorder();
133
+ await assert.rejects(player.play("/tmp/s.wav", undefined, () => false), /permit/);
134
+ assert.equal(spawned.length, 0);
135
+ const { player: second, spawned: secondSpawned, children } = recorder();
136
+ const playing = second.play("/tmp/s.wav", undefined, { permit: () => true });
137
+ assert.equal(secondSpawned.length, 1);
138
+ children[0]!.stdout.emit("data", JSON.stringify({ schema: NATIVE_PULSE_SCHEMA, ok: true, played: true }));
139
+ children[0]!.emit("close");
140
+ await playing;
141
+ });
142
+
143
+ test("malformed, oversize and error output fail privately and kill at most once", async () => {
144
+ const malformed = recorder();
145
+ const malformedProbe = malformed.player.probe();
146
+ malformed.children[0]!.stdout.emit("data", "not json");
147
+ malformed.children[0]!.emit("close");
148
+ assert.deepEqual(await malformedProbe, { available: false, formats: [] });
149
+ assert.equal(malformed.player.getNativeFormats().length, 0);
150
+
151
+ const oversize = recorder();
152
+ const oversizeProbe = oversize.player.probe();
153
+ oversize.children[0]!.stdout.emit("data", "x".repeat(2048));
154
+ assert.equal(oversize.children[0]!.kills.length, 1);
155
+ assert.equal(oversize.children[0]!.kills[0], "SIGKILL");
156
+ oversize.children[0]!.emit("close");
157
+ assert.deepEqual(await oversizeProbe, { available: false, formats: [] });
158
+
159
+ const failure = recorder();
160
+ const playing = failure.player.play("/private/snapshot.wav");
161
+ failure.children[0]!.stdout.emit("data", JSON.stringify({ schema: NATIVE_PULSE_SCHEMA, ok: false }));
162
+ failure.children[0]!.emit("close");
163
+ await assert.rejects(playing, (error: Error) => /Native pulse/.test(error.message) && !/snapshot|cookie|\/private/.test(error.message));
164
+ });
165
+
166
+ test("timeout and abort SIGKILL once and settle only on close", async () => {
167
+ const timed = recorder("linux", { probeTimeoutMs: 15 });
168
+ const probing = timed.player.probe();
169
+ await new Promise(resolve => setTimeout(resolve, 45));
170
+ assert.deepEqual(timed.children[0]!.kills, ["SIGKILL"]);
171
+ let settled = false; void probing.then(() => { settled = true; });
172
+ timed.children[0]!.emit("exit", 0);
173
+ timed.children[0]!.emit("error", new Error("spawn failed")); // Node emits close after error
174
+ await new Promise(resolve => setTimeout(resolve, 0));
175
+ assert.equal(settled, false);
176
+ timed.children[0]!.emit("close");
177
+ assert.deepEqual(await probing, { available: false, formats: [] });
178
+
179
+ const aborted = recorder();
180
+ const controller = new AbortController();
181
+ const playing = aborted.player.play("/tmp/s.wav", controller.signal);
182
+ controller.abort();
183
+ assert.deepEqual(aborted.children[0]!.kills, ["SIGKILL"]);
184
+ aborted.children[0]!.emit("close");
185
+ await assert.rejects(playing, /aborted|Native pulse/);
186
+ });
187
+
188
+ test("default play deadline is 11000 ms and outlives the accepted 10 s maximum", async () => {
189
+ let scheduledMs = 0; let fire: (() => void) | undefined;
190
+ const { player, children } = recorder("linux", {
191
+ setTimeout: (callback, ms) => { scheduledMs = ms; fire = callback; return 1; },
192
+ clearTimeout: () => {},
193
+ });
194
+ const playing = player.play("/tmp/s.wav", undefined, () => true);
195
+ assert.equal(children.length, 1);
196
+ assert.equal(scheduledMs, 11000);
197
+ assert.ok(scheduledMs > 10000, "the native bridge must outlive the accepted 10 s file");
198
+ assert.deepEqual(children[0]!.kills, []);
199
+ fire!();
200
+ assert.deepEqual(children[0]!.kills, ["SIGKILL"]);
201
+ children[0]!.emit("close");
202
+ await assert.rejects(playing, /Native pulse/);
203
+ });
204
+
205
+ test("probe never throws for offline/malformed IPC while play rejects and cleans", async () => {
206
+ const { player, children } = recorder();
207
+ const probing = player.probe();
208
+ children[0]!.stdout.emit("data", JSON.stringify({ schema: "wrong", ok: true, available: true, formats: ["wav"] }));
209
+ children[0]!.emit("close");
210
+ assert.deepEqual(await probing, { available: false, formats: [] });
211
+ const { player: playPlayer, children: playChildren } = recorder();
212
+ const playing = playPlayer.play("/tmp/s.wav");
213
+ playChildren[0]!.stdout.emit("data", JSON.stringify({ schema: "wrong", ok: true, played: true }));
214
+ playChildren[0]!.emit("close");
215
+ await assert.rejects(playing, /Native pulse/);
216
+ });
217
+
218
+ test("bridge and worker import no SDK, UI, or addon surface", () => {
219
+ for (const path of ["lib/notification-audio-native.ts", "lib/notification-pulse-worker.ts"]) {
220
+ const source = readFileSync(join(process.cwd(), path), "utf8");
221
+ assert.doesNotMatch(source, /@earendil-works/, path);
222
+ assert.doesNotMatch(source, /from\s+["'][^"']*extensions\//, path);
223
+ assert.doesNotMatch(source, /native\/|\.node["']/, path);
224
+ }
225
+ });
226
+
227
+ test("child environment is an allowlist: Pulse vars kept, credentials and debug dropped", async () => {
228
+ const captured: NodeJS.ProcessEnv[] = [];
229
+ const player = new NativePulsePlayer({
230
+ platform: "linux",
231
+ spawn: (_executable, _args, options) => { captured.push(options.env); return new FakeChild(); },
232
+ env: { PULSE_SERVER: "unix:/s", PULSE_COOKIE: "/c", XDG_RUNTIME_DIR: "/r", XDG_CONFIG_HOME: "/cfg", HOME: "/h", LANG: "C",
233
+ OPENAI_API_KEY: "sk", ANTHROPIC_API_KEY: "x", AWS_SECRET_ACCESS_KEY: "y", NODE_OPTIONS: "--inspect", NODE_PATH: "/n", DEBUG: "*", PATH: "/bin" },
234
+ });
235
+ void player.probe();
236
+ const env = captured[0]!;
237
+ assert.equal(env.PULSE_SERVER, "unix:/s"); assert.equal(env.PULSE_COOKIE, "/c");
238
+ assert.equal(env.XDG_RUNTIME_DIR, "/r"); assert.equal(env.XDG_CONFIG_HOME, "/cfg"); assert.equal(env.HOME, "/h"); assert.equal(env.LANG, "C");
239
+ for (const drop of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AWS_SECRET_ACCESS_KEY", "NODE_OPTIONS", "NODE_PATH", "DEBUG", "PATH"])
240
+ assert.equal(env[drop], undefined, drop);
241
+ assert.equal(env.NODE_NO_WARNINGS, "1"); assert.equal(env.GENTLE_PI_AGENTS_CHILD, "1");
242
+ });
243
+
244
+ test("real worker probe against a fake Unix socket authenticates without CREATE", async () => {
245
+ const server = await startFakeServer();
246
+ try {
247
+ // Exercise the Linux Pulse transport against a fake socket on any Unix host.
248
+ const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, probeTimeoutMs: 8000 });
249
+ const result = await player.probe();
250
+ assert.deepEqual(result, { available: true, formats: ["wav"] });
251
+ assert.deepEqual(server.seen, [8, 9, 20]);
252
+ assert.ok(!server.seen.includes(3), "probe must not create a stream");
253
+ } finally { await server.close(); }
254
+ });
255
+
256
+ test("real worker plays a fake PCM snapshot to a fake socket with no OS audio", async () => {
257
+ const bytes = wav(16, 1, 8000, 800);
258
+ const dir = mkdtempSync(join(tmpdir(), "native-pulse-snap-"));
259
+ const snapshot = join(dir, "sound.wav");
260
+ writeFileSync(snapshot, bytes, { mode: 0o400 });
261
+ const server = await startFakeServer();
262
+ try {
263
+ const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, playTimeoutMs: 8000 });
264
+ await player.play(snapshot);
265
+ assert.deepEqual(Buffer.concat(server.pcm), dataOf(bytes));
266
+ assert.ok(server.seen.includes(3) && server.seen.includes(12) && server.seen.includes(4));
267
+ assert.ok(existsSync(snapshot), "the bridge/worker must not delete the snapshot; the owner does");
268
+ } finally { await server.close(); rmSync(dir, { recursive: true, force: true }); }
269
+ });
270
+
271
+ test("real worker rejects invalid WAV before opening the socket", async () => {
272
+ const dir = mkdtempSync(join(tmpdir(), "native-pulse-bad-"));
273
+ const snapshot = join(dir, "sound.wav");
274
+ writeFileSync(snapshot, Buffer.alloc(64, 0x00), { mode: 0o400 });
275
+ const server = await startFakeServer();
276
+ try {
277
+ const player = new NativePulsePlayer({ platform: "linux", env: { PULSE_SERVER: `unix:${server.path}` }, playTimeoutMs: 8000 });
278
+ await assert.rejects(player.play(snapshot), /Native pulse/);
279
+ assert.deepEqual(server.seen, []);
280
+ } finally { await server.close(); rmSync(dir, { recursive: true, force: true }); }
281
+ });