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,123 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
5
+ import { NATIVE_REVIEW_MODE_SOURCE, type NativeReviewCli } from "../lib/native-review-cli.ts";
6
+ import {
7
+ decodeAgentRiskEscalation,
8
+ escalatedRisk,
9
+ HIGH_RISK_ITEMS,
10
+ REVIEW_ASSESSMENT_SCHEMA,
11
+ VERIFICATION_TIER,
12
+ } from "../lib/review-risk-assessment.ts";
13
+
14
+ // gentle-shell#1494 S6: the agent may raise a candidate's risk to high by
15
+ // citing one item of the shared high-risk list, never lower it. The raise
16
+ // travels inside the existing assess call; no new operation.
17
+
18
+ test("the high-risk list has exactly the six items of the always-on Task Size section", () => {
19
+ assert.deepEqual(Object.keys(HIGH_RISK_ITEMS).map(Number), [1, 2, 3, 4, 5, 6]);
20
+ assert.equal(HIGH_RISK_ITEMS[2], "security");
21
+ assert.equal(HIGH_RISK_ITEMS[6], "no test would catch a regression");
22
+ });
23
+
24
+ test("decodeAgentRiskEscalation accepts an item from 1 to 6 with a non-empty reason", () => {
25
+ assert.deepEqual(decodeAgentRiskEscalation({ item: 2, reason: "weakens the permission check in utils.ts" }), {
26
+ item: 2,
27
+ reason: "weakens the permission check in utils.ts",
28
+ });
29
+ });
30
+
31
+ test("decodeAgentRiskEscalation rejects unknown items, empty or oversized reasons, and extra fields", () => {
32
+ for (const bad of [
33
+ { item: 0, reason: "x" },
34
+ { item: 7, reason: "x" },
35
+ { item: 2.5, reason: "x" },
36
+ { item: "2", reason: "x" },
37
+ { item: 2, reason: " " },
38
+ { item: 2, reason: "x".repeat(501) },
39
+ { item: 2, reason: "x", tier: "low" },
40
+ { item: 2 },
41
+ null,
42
+ "high",
43
+ ]) {
44
+ assert.throws(() => decodeAgentRiskEscalation(bad), /escalate/, `must reject ${JSON.stringify(bad)}`);
45
+ }
46
+ });
47
+
48
+ test("escalatedRisk raises passive and medium to high and never lowers", () => {
49
+ const escalation = { item: 3 as const, reason: "changes a published export" };
50
+ assert.equal(escalatedRisk(VERIFICATION_TIER.PASSIVE, escalation), VERIFICATION_TIER.HIGH);
51
+ assert.equal(escalatedRisk(VERIFICATION_TIER.MEDIUM, escalation), VERIFICATION_TIER.HIGH);
52
+ assert.equal(escalatedRisk(VERIFICATION_TIER.HIGH, escalation), VERIFICATION_TIER.HIGH);
53
+ assert.equal(escalatedRisk(VERIFICATION_TIER.UNASSESSABLE, escalation), VERIFICATION_TIER.UNASSESSABLE);
54
+ for (const tier of Object.values(VERIFICATION_TIER)) assert.equal(escalatedRisk(tier, undefined), tier);
55
+ });
56
+
57
+ function assessTool(): { execute: (id: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details: unknown }> } {
58
+ const tools = new Map<string, any>();
59
+ const pi = { on() {}, registerCommand() {}, registerTool(tool: { name: string }) { tools.set(tool.name, tool); } } as unknown as ExtensionAPI;
60
+ const nativeReviewCli: Partial<NativeReviewCli> = {
61
+ reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: "off", cloneLocal: "off", effective: "off", source: NATIVE_REVIEW_MODE_SOURCE.GLOBAL } }),
62
+ assess: async () => ({
63
+ schema: REVIEW_ASSESSMENT_SCHEMA,
64
+ risk: "medium",
65
+ reasons: [],
66
+ changedPaths: 1,
67
+ changedLines: 3,
68
+ candidate: { kind: "current-changes", baseRef: undefined },
69
+ }),
70
+ };
71
+ createGentleAiExtension({ nativeReviewCli: nativeReviewCli as NativeReviewCli })(pi);
72
+ return tools.get("gentle_review");
73
+ }
74
+
75
+ const ctx = { cwd: process.cwd() } as ExtensionContext;
76
+
77
+ test("gentle_review assess: an agent escalation raises a medium candidate to high and records why", async () => {
78
+ const tool = assessTool();
79
+ const input = JSON.stringify({ writerModelId: "openai/gpt-6.1-sol", writerEffort: "high", escalate: { item: 2, reason: "weakens the permission check in utils.ts" } });
80
+ const { details } = await tool.execute("esc-1", { operation: "assess", input }, undefined, undefined, ctx);
81
+ const plan = details as { risk: string; nativeRisk: string; agentEscalation: { item: number; label: string; reason: string }; plan: { independentVerifier: boolean } };
82
+ assert.equal(plan.nativeRisk, "medium");
83
+ assert.equal(plan.risk, VERIFICATION_TIER.HIGH);
84
+ assert.deepEqual(plan.agentEscalation, { item: 2, label: "security", reason: "weakens the permission check in utils.ts", applied: true });
85
+ assert.equal(plan.plan.independentVerifier, true, "a raised candidate always gets the independent verifier");
86
+ });
87
+
88
+ test("gentle_review assess: without an escalation the native tier stands", async () => {
89
+ const tool = assessTool();
90
+ const input = JSON.stringify({ writerModelId: "openai/gpt-6.1-sol", writerEffort: "high" });
91
+ const { details } = await tool.execute("esc-2", { operation: "assess", input }, undefined, undefined, ctx);
92
+ const plan = details as { risk: string; nativeRisk: string; agentEscalation?: unknown; plan: { independentVerifier: boolean } };
93
+ assert.equal(plan.risk, "medium");
94
+ assert.equal(plan.nativeRisk, "medium");
95
+ assert.equal(plan.agentEscalation, undefined);
96
+ assert.equal(plan.plan.independentVerifier, false);
97
+ });
98
+
99
+ test("gentle_review assess: a malformed escalation is rejected instead of ignored", async () => {
100
+ const tool = assessTool();
101
+ await assert.rejects(
102
+ () => tool.execute("esc-3", { operation: "assess", input: JSON.stringify({ escalate: { item: 9, reason: "x" } }) }, undefined, undefined, ctx),
103
+ /escalate/,
104
+ );
105
+ });
106
+
107
+ test("the rule text tells the agent when and how to escalate, and that it never lowers", async () => {
108
+ const { readFileSync } = await import("node:fs");
109
+ const verification = readFileSync(new URL("../assets/orchestrator-verification.md", import.meta.url), "utf8");
110
+ const tracking = readFileSync(new URL("../assets/orchestrator-tracking.md", import.meta.url), "utf8");
111
+ const extension = readFileSync(new URL("../extensions/gentle-ai.ts", import.meta.url), "utf8");
112
+ assert.match(verification, /"escalate": \{"item": <1-6>, "reason": "<one line>"\}/);
113
+ assert.match(verification, /never lowers a tier/);
114
+ assert.match(tracking, /assess `escalate` field \(`orchestrator-verification\.md`\); never lower one/);
115
+ assert.match(extension, /\{"escalate":\{"item":1-6,"reason":"<one line>"\}\}/);
116
+ });
117
+
118
+ test("gentle_review assess: agentEscalation says whether the escalation changed the tier", async () => {
119
+ const tool = assessTool();
120
+ const input = JSON.stringify({ escalate: { item: 4, reason: "reorders an async write" } });
121
+ const { details } = await tool.execute("esc-5", { operation: "assess", input }, undefined, undefined, ctx);
122
+ assert.equal((details as { agentEscalation: { applied: boolean } }).agentEscalation.applied, true, "medium raised to high is applied");
123
+ });
@@ -92,3 +92,20 @@ test("dropAll discards pending, consumed, and delivered state", () => {
92
92
  queue.enqueue(task("t1"), 5000);
93
93
  assert.deepEqual(queue.takeDeliverable(5000).map((entry) => entry.task.id), ["t1"], "the queue is fresh after dropAll");
94
94
  });
95
+
96
+ test("requeue returns a failed delivery once, keeps its settle time, and respects consume", () => {
97
+ const queue = createCompletionQueue<FakeTask>();
98
+ queue.enqueue(task("t1"), 1000);
99
+ queue.enqueue(task("t2"), 1000);
100
+ const [first, second] = queue.takeDeliverable(2000);
101
+ queue.requeue(first!);
102
+ queue.requeue(first!);
103
+ queue.requeue(second!);
104
+ queue.consume("t2");
105
+ const retried = queue.takeDeliverable(1000 + STALE_COMPLETION_MS);
106
+ assert.deepEqual(retried.map((entry) => entry.task.id), ["t1"], "a double requeue yields one entry and a consumed one is dropped");
107
+ assert.equal(retried[0]!.settledAt, 1000, "the original settle time is kept");
108
+ assert.equal(retried[0]!.stale, true, "a retried completion can become stale");
109
+ queue.enqueue(task("t1"), 5000);
110
+ assert.deepEqual(queue.takeDeliverable(5000), [], "a delivered retry is still delivered at most once");
111
+ });
@@ -9,7 +9,7 @@ export interface FakeChild {
9
9
  child: ChildLike;
10
10
  written: Array<Record<string, unknown>>;
11
11
  emit(event: Record<string, unknown>): void;
12
- exit(code: number): void;
12
+ exit(code: number | null, signal?: string | null): void;
13
13
  fail(message: string): void;
14
14
  killed: string[];
15
15
  sent: Array<Record<string, unknown>>;
@@ -62,5 +62,5 @@ export function fakeChild(options: { exitOnKill?: boolean; pid?: number } = {}):
62
62
  return child;
63
63
  },
64
64
  };
65
- return { child, written, killed, sent, get disconnects() { return disconnects; }, message: (event) => emitter.emit("message", event), emit: (event) => stdout.write(`${JSON.stringify(event)}\n`), exit: (code) => emitter.emit("exit", code, null), fail: (message) => emitter.emit("error", new Error(message)) };
65
+ return { child, written, killed, sent, get disconnects() { return disconnects; }, message: (event) => emitter.emit("message", event), emit: (event) => stdout.write(`${JSON.stringify(event)}\n`), exit: (code, signal = null) => emitter.emit("exit", code, signal), fail: (message) => emitter.emit("error", new Error(message)) };
66
66
  }
@@ -0,0 +1,106 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createAgentMessageQueue, STALE_NOTIFICATION_MS } from "../lib/agents-message-delivery.ts";
4
+
5
+ test("delivers live notifications and queries in order for the active session", () => {
6
+ const queue = createAgentMessageQueue();
7
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "First progress", 1000);
8
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Which branch?", 1050);
9
+
10
+ assert.equal(queue.pendingCount(), 2);
11
+ const deliverable = queue.takeDeliverable(1100, "s1", () => true);
12
+ assert.equal(deliverable.length, 2);
13
+
14
+ assert.equal(deliverable[0]?.kind, "notification");
15
+ assert.equal(deliverable[0]?.content, "First progress");
16
+ assert.equal(deliverable[0]?.display, false);
17
+
18
+ assert.equal(deliverable[1]?.kind, "query");
19
+ assert.match(deliverable[1]?.content ?? "", /Which branch\?/);
20
+ assert.equal(deliverable[1]?.display, true);
21
+ assert.equal(deliverable[1]?.requestId, "q1");
22
+
23
+ assert.equal(queue.pendingCount(), 0);
24
+ assert.equal(queue.takeDeliverable(1200, "s1", () => true).length, 0, "messages are delivered at most once");
25
+ });
26
+
27
+ test("a notification from a task that settled before delivery is dropped silently", () => {
28
+ const queue = createAgentMessageQueue();
29
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Late notice", 1000);
30
+
31
+ // The task finishes and is invalidated before the parent turn boundary.
32
+ queue.invalidateTask("task-1");
33
+
34
+ const deliverable = queue.takeDeliverable(1050, "s1", () => false);
35
+ assert.equal(deliverable.length, 0, "notification from settled task must never enter conversation");
36
+ });
37
+
38
+ test("an answered query is consumed and never delivered", () => {
39
+ const queue = createAgentMessageQueue();
40
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Which target?", 1000);
41
+
42
+ // Parent replies before queue flush.
43
+ queue.consumeQuery("task-1", "q1");
44
+
45
+ const deliverable = queue.takeDeliverable(1050, "s1", () => true);
46
+ assert.equal(deliverable.length, 0, "answered query must not be delivered");
47
+ });
48
+
49
+ test("an expired query is dropped and never delivered", () => {
50
+ const queue = createAgentMessageQueue();
51
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Need approval", 1000);
52
+
53
+ // Query times out.
54
+ queue.expireQuery("task-1", "q1");
55
+
56
+ const deliverable = queue.takeDeliverable(1050, "s1", () => true);
57
+ assert.equal(deliverable.length, 0, "expired query must not be delivered");
58
+ });
59
+
60
+ test("a notification exceeding the stale window is dropped", () => {
61
+ const queue = createAgentMessageQueue();
62
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Slow notice", 1000);
63
+
64
+ // Held past the 90-second threshold.
65
+ const deliverable = queue.takeDeliverable(1000 + STALE_NOTIFICATION_MS + 10, "s1", () => true);
66
+ assert.equal(deliverable.length, 0, "stale notification must not be delivered");
67
+ });
68
+
69
+ test("isTaskLive returning false drops messages for non-live tasks", () => {
70
+ const queue = createAgentMessageQueue();
71
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Dead task notice", 1000);
72
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Dead task query", 1000);
73
+
74
+ const deliverable = queue.takeDeliverable(1050, "s1", (id) => id !== "task-1");
75
+ assert.equal(deliverable.length, 0, "non-live task messages must be dropped");
76
+ });
77
+
78
+ test("session mismatch drops foreign session messages", () => {
79
+ const queue = createAgentMessageQueue();
80
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "session-old" }, "Old session notice", 1000);
81
+
82
+ const deliverable = queue.takeDeliverable(1050, "session-new", () => true);
83
+ assert.equal(deliverable.length, 0, "foreign session message must be dropped");
84
+ });
85
+
86
+ test("deduplicates identical query requests for the same task and requestId", () => {
87
+ const queue = createAgentMessageQueue();
88
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "First ask", 1000);
89
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Duplicate ask", 1001);
90
+
91
+ assert.equal(queue.pendingCount(), 1);
92
+ const deliverable = queue.takeDeliverable(1050, "s1", () => true);
93
+ assert.equal(deliverable.length, 1);
94
+ assert.match(deliverable[0]!.content, /First ask/);
95
+ });
96
+
97
+ test("dropAll clears everything across session reset", () => {
98
+ const queue = createAgentMessageQueue();
99
+ queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Notice", 1000);
100
+ queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Query", 1000);
101
+ assert.equal(queue.pendingCount(), 2);
102
+
103
+ queue.dropAll();
104
+ assert.equal(queue.pendingCount(), 0);
105
+ assert.equal(queue.takeDeliverable(1050, "s1", () => true).length, 0);
106
+ });
@@ -3,7 +3,9 @@ import test from "node:test";
3
3
  import {
4
4
  applyTaskEvent,
5
5
  emptyThread,
6
+ MISSING_TOOLS_NOTE_PREFIX,
6
7
  normalizeRpcEvent,
8
+ ToolArgumentProgress,
7
9
  TASK_EVENT,
8
10
  TASK_STATUS,
9
11
  taskLabel,
@@ -12,6 +14,51 @@ import {
12
14
  type TaskRecord,
13
15
  } from "../lib/agents-protocol.ts";
14
16
 
17
+ test("status observers see only live transitions, remain isolated and cannot fail mutations", () => {
18
+ const store = new TaskStore();
19
+ const changes: Array<{ status: string; sequence: number; parentSessionId: string; producerId: string; runId: string }> = [];
20
+ store.subscribeStatusChanges(() => { throw new Error("listener failure"); });
21
+ store.subscribeStatusChanges(async () => { throw new Error("async failure"); });
22
+ const stop = store.subscribeStatusChanges(change => { changes.push(change); });
23
+ store.restore(record({ id: "restored" }), emptyThread());
24
+ store.add(record());
25
+ store.update("t1", { status: TASK_STATUS.RUNNING });
26
+ store.update("t1", { status: TASK_STATUS.RUNNING, label: "new label" });
27
+ const ask = { type: TASK_EVENT.ASK, request: { id: "q", method: "input", title: "Question" } } as const;
28
+ store.apply("t1", ask, 2);
29
+ store.apply("t1", ask, 3);
30
+ store.apply("t1", { type: TASK_EVENT.TEXT, text: "answer" }, 4);
31
+ store.apply("t1", { type: TASK_EVENT.TEXT, text: "more" }, 5);
32
+ store.add(record({ id: "other", parentSessionId: "s2" }));
33
+ assert.deepEqual(changes.map(change => change.status), ["queued", "running", "waiting", "running", "queued"]);
34
+ assert.deepEqual(changes.map(change => change.sequence), [1, 2, 3, 4, 5]);
35
+ assert.deepEqual(changes.map(change => change.parentSessionId), ["s1", "s1", "s1", "s1", "s2"]);
36
+ assert.equal(new Set(changes.slice(0, 4).map(change => change.runId)).size, 1);
37
+ assert.equal(store.get("t1")?.status, "running");
38
+ const another = new TaskStore();
39
+ let otherProducer: string | undefined;
40
+ another.subscribeStatusChanges(change => { otherProducer = change.producerId; });
41
+ another.add(record());
42
+ assert.notEqual(otherProducer, changes[0].producerId);
43
+ stop();
44
+ store.update("t1", { status: TASK_STATUS.COMPLETED });
45
+ assert.equal(changes.length, 5);
46
+ });
47
+
48
+ test("all terminal statuses produce exactly one transition and restore establishes a silent baseline", () => {
49
+ for (const status of [TASK_STATUS.COMPLETED, TASK_STATUS.FAILED, TASK_STATUS.CANCELLED, TASK_STATUS.TIMED_OUT]) {
50
+ const store = new TaskStore();
51
+ const statuses: string[] = [];
52
+ store.subscribeStatusChanges(change => { statuses.push(change.status); });
53
+ store.restore(record({ status: TASK_STATUS.RUNNING }), emptyThread());
54
+ store.update("t1", { status });
55
+ store.update("t1", { status, result: "report" });
56
+ store.add(record({ status }));
57
+ assert.deepEqual(statuses, [status]);
58
+ assert.equal(store.get("t1")?.status, status);
59
+ }
60
+ });
61
+
15
62
  // Gentle Agents protocol: the child pi process streams RPC events; the host
16
63
  // normalizes them into small typed deltas, applies them to an append-only
17
64
  // thread, and notifies only the listeners of the task that changed.
@@ -67,6 +114,56 @@ test("normalizeRpcEvent maps pi RPC events to task deltas and ignores the rest",
67
114
  assert.deepEqual(normalizeRpcEvent("garbage"), []);
68
115
  });
69
116
 
117
+ // #1690: Pi drops unknown --tools names silently, so the child reports them
118
+ // with one marked notify; only that notify reaches the task thread.
119
+ test("normalizeRpcEvent turns the marked missing-tools notify into a note and drops every other notify", () => {
120
+ const message = `${MISSING_TOOLS_NOTE_PREFIX} gentle_review_scope, codegraph`;
121
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u1", method: "notify", message, notifyType: "warning" }), [{ type: TASK_EVENT.NOTE, text: message }]);
122
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u2", method: "notify", message: "requested tools missing in child: x" }), [], "an unmarked notify stays dropped");
123
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u3", method: "notify", message: ` ${MISSING_TOOLS_NOTE_PREFIX} x` }), [], "the marker must be a prefix");
124
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u4", method: "setStatus", statusKey: "k", statusText: `${MISSING_TOOLS_NOTE_PREFIX} x` }), [], "only notify carries the marker");
125
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u5", method: "notify", message: `${MISSING_TOOLS_NOTE_PREFIX} \u001B[2Jx` }), [{ type: TASK_EVENT.NOTE, text: `${MISSING_TOOLS_NOTE_PREFIX} x` }], "child text is sanitized");
126
+ });
127
+
128
+ test("argument liveness accepts current and legacy Pi starts without display or usage deltas", () => {
129
+ for (const start of [{ type: "toolcall_start", contentIndex: 0, id: "c1", toolName: "write" },
130
+ { type: "toolcall_start", contentIndex: 0, partial: { content: [{ type: "toolCall", id: "c1", name: "write", arguments: {} }] } }]) {
131
+ const progress = new ToolArgumentProgress();
132
+ const update = (inner: Record<string, unknown>) => ({ type: "message_update", assistantMessageEvent: inner });
133
+ const delta = update({ type: "toolcall_delta", contentIndex: 0, delta: "private" });
134
+ assert.equal(progress.observe(delta), false, "no active generation");
135
+ progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
136
+ assert.equal(progress.observe(delta), false, "no announced block");
137
+ assert.equal(progress.observe(update(start)), false, "announcement alone is not progress");
138
+ assert.equal(progress.observe(delta), true);
139
+ assert.equal(progress.observe(delta), false);
140
+ assert.deepEqual(normalizeRpcEvent(delta), [], "no arguments in display events or provisional totals");
141
+ progress.observe(update({ type: "toolcall_end", contentIndex: 0 }));
142
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "late" })), false);
143
+ progress.observe(update(start));
144
+ assert.equal(progress.observe(delta), false, "a closed block cannot be reopened");
145
+ }
146
+ });
147
+
148
+ test("argument liveness fails closed on malformed starts, stale generations and fingerprint exhaustion", () => {
149
+ const progress = new ToolArgumentProgress();
150
+ const update = (inner: Record<string, unknown>) => ({ type: "message_update", assistantMessageEvent: inner });
151
+ for (const timestamp of [undefined, "1", -1, 1.5]) progress.observe({ type: "message_start", message: { role: "assistant", timestamp } });
152
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "x" })), false);
153
+ progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
154
+ for (const fields of [{ id: 1, toolName: "write" }, { id: "c", toolName: "" }, { partial: null }]) {
155
+ progress.observe(update({ type: "toolcall_start", contentIndex: 0, ...fields }));
156
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "x" })), false);
157
+ }
158
+ progress.observe(update({ type: "toolcall_start", contentIndex: 0, id: "c", toolName: "write" }));
159
+ for (let index = 0; index < 4096; index++) assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: String(index) })), true);
160
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "over-cap" })), false);
161
+ progress.observe({ type: "message_end" });
162
+ progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
163
+ progress.observe(update({ type: "toolcall_start", contentIndex: 0, id: "c", toolName: "write" }));
164
+ assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "stale" })), false);
165
+ });
166
+
70
167
  test("response observations are opt-in, finalized, field-specific and content-free", () => {
71
168
  const message = { role: "assistant", provider: "openai", model: "gpt-4o", responseModel: "gpt-4o-2024-08-06",
72
169
  providerThinkingLevel: "high", stopReason: "stop", content: [{ type: "text", text: "private response" }],
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
3
+ import { AGENT_MODE, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
4
4
  import { CHILD_MESSAGE_MAX_BYTES, ChildMessenger } from "../lib/agents-messaging.ts";
5
5
  import { TaskStore } from "../lib/agents-protocol.ts";
6
6
  import { AgentRunner } from "../lib/agents-runner.ts";
@@ -8,7 +8,7 @@ import { fakeChild } from "./agents-fake-child.ts";
8
8
 
9
9
  const tick = () => new Promise((resolve) => setImmediate(resolve));
10
10
  const agent: AgentDefinition = { name: "worker", description: "", filePath: "/a.md", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: ["read"] };
11
- const request = (mode = AGENT_MODE.BACKGROUND) => ({ agent, prompt: "work", label: undefined, context: undefined, mode, cwd: "/repo", parentSessionId: "s1", model: undefined, thinking: undefined, sessionDir: "/sessions", resumeSessionPath: undefined, env: {} });
11
+ const request = (mode: AgentMode = AGENT_MODE.BACKGROUND) => ({ agent, prompt: "work", label: undefined, context: undefined, mode, cwd: "/repo", parentSessionId: "s1", model: undefined, thinking: undefined, sessionDir: "/sessions", resumeSessionPath: undefined, env: {} });
12
12
 
13
13
  test("ChildMessenger validates query replies and settles timeout, callback failure, and disconnect once", async () => {
14
14
  const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
@@ -74,7 +74,7 @@ test("ChildMessenger rejects an empty reply error and ignores mixed reply fields
74
74
 
75
75
  test("runner sends bounded static query rejection frames that settle the child before timeout", async () => {
76
76
  const child = fakeChild();
77
- const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
77
+ const listeners = new Map<string, Array<(value: unknown) => void>>();
78
78
  const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
79
79
  child.child.send = (frame, done) => {
80
80
  child.sent.push(frame);
@@ -83,8 +83,8 @@ test("runner sends bounded static query rejection frames that settle the child b
83
83
  return true;
84
84
  };
85
85
  const messenger = new ChildMessenger({
86
- send: (frame) => { child.message(frame); return true; },
87
- on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener as (value: Record<string, unknown>) => void]),
86
+ send: (frame: Record<string, unknown>) => { child.message(frame); return true; },
87
+ on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener]),
88
88
  }, (fn) => {
89
89
  const timer = { cancelled: false, fn };
90
90
  timers.push(timer);
@@ -102,7 +102,7 @@ test("runner sends bounded static query rejection frames that settle the child b
102
102
 
103
103
  test("a malformed query with a valid q ID gets a reply that clears the matching child query", async () => {
104
104
  const child = fakeChild();
105
- const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
105
+ const listeners = new Map<string, Array<(value: unknown) => void>>();
106
106
  const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
107
107
  child.child.send = (frame, done) => {
108
108
  child.sent.push(frame);
@@ -111,8 +111,8 @@ test("a malformed query with a valid q ID gets a reply that clears the matching
111
111
  return true;
112
112
  };
113
113
  const messenger = new ChildMessenger({
114
- send: (frame) => { child.message(frame); return true; },
115
- on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener as (value: Record<string, unknown>) => void]),
114
+ send: (frame: Record<string, unknown>) => { child.message(frame); return true; },
115
+ on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener]),
116
116
  }, (fn) => {
117
117
  const timer = { cancelled: false, fn };
118
118
  timers.push(timer);
@@ -133,7 +133,7 @@ test("background queries keep correlation, bounds, and ownership before replying
133
133
  const child = fakeChild();
134
134
  const queries: Array<{ taskId: string; requestId: string; message: string }> = [];
135
135
  const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => child.child, now: () => 1, schedule: () => () => {}, pi: { command: "pi", args: [] } }, {
136
- askUser: async () => ({ cancelled: true }), onQuery: (task, requestId, message) => queries.push({ taskId: task.id, requestId, message }),
136
+ askUser: async () => ({ cancelled: true }), onQuery: (task, requestId, message) => { queries.push({ taskId: task.id, requestId, message }); },
137
137
  });
138
138
  const task = runner.run(request());
139
139
  await tick();
@@ -5,6 +5,7 @@ import test from "node:test";
5
5
  import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
6
6
  import { AgentRunner, type ChildLike, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
7
7
  import { TASK_STATUS, TaskStore } from "../lib/agents-protocol.ts";
8
+ import { fakeChild } from "./agents-fake-child.ts";
8
9
 
9
10
  const fixture = fileURLToPath(new URL("./fixtures/agents-process-child.mjs", import.meta.url));
10
11
  const agent: AgentDefinition = { name: "process", description: "test", filePath: "/test.md", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: [] };
@@ -18,6 +19,36 @@ const waitFor = async (predicate: () => boolean, timeoutMs = 10_000): Promise<vo
18
19
  }
19
20
  };
20
21
 
22
+ test("missing RPC streams fail the task without announcing a launch or leaking its queue slot", async () => {
23
+ const store = new TaskStore();
24
+ const child = fakeChild();
25
+ let launched = false;
26
+ const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 1000 }, {
27
+ spawn: () => ({ ...child.child, stdin: null, stdout: null }), now: Date.now,
28
+ schedule: (fn, ms) => { const timer = setTimeout(fn, ms); return () => clearTimeout(timer); },
29
+ pi: { command: "pi", args: [] },
30
+ }, { askUser: async () => ({ cancelled: true }) });
31
+ const task = runner.run({ ...request("missing streams"), onLaunch: () => { launched = true; } });
32
+ await waitFor(() => store.get(task.id)?.status === TASK_STATUS.FAILED);
33
+ assert.equal(launched, false);
34
+ assert.match(store.get(task.id)!.error!, /RPC streams/);
35
+ assert.ok(child.killed.length > 0, "the malformed child is still stopped");
36
+ });
37
+
38
+ test("a real failed OS spawn has no pid and is retained as a launch failure", async () => {
39
+ const store = new TaskStore();
40
+ let launched = false;
41
+ const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 1000 }, {
42
+ spawn: (_command, _args, options) => nodeSpawn("/nonexistent/gentle-pi-child", [], { ...options, stdio: ["pipe", "pipe", "pipe", "ipc"] }),
43
+ now: Date.now, schedule: (fn, ms) => { const timer = setTimeout(fn, ms); return () => clearTimeout(timer); },
44
+ pi: { command: "unused", args: [] },
45
+ }, { askUser: async () => ({ cancelled: true }) });
46
+ const task = runner.run({ ...request("failed spawn"), onLaunch: () => { launched = true; } });
47
+ await waitFor(() => store.get(task.id)?.status === TASK_STATUS.FAILED);
48
+ assert.equal(launched, false);
49
+ assert.match(store.get(task.id)!.error!, /ENOENT/);
50
+ });
51
+
21
52
  test("POSIX cleanup retains queue slots when a leader exits but its TERM-resisting descendant remains", { skip: process.platform === "win32" }, async () => {
22
53
  const store = new TaskStore();
23
54
  let launches = 0;