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,202 @@
1
+ // Model-facing surface of background jobs: the `bash_background`, `job_stop`,
2
+ // and `job_list` tools, the exit notice text, and its transcript card.
3
+ // Delivery of the notice belongs to the caller (Gentle Agents' parent
4
+ // delivery router), so this module never sends messages itself.
5
+
6
+ import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { BASH_BACKGROUND_TOOL, MAX_RUNNING_JOBS, MONITOR_TOOL, type JobRecord, type JobRegistry } from "./background-jobs.ts";
8
+ import { MONITOR_BATCH_MS, MONITOR_FLOOD_LINES, MONITOR_FLOOD_WINDOW_MS, MONITOR_LINES_PER_NOTICE, MONITOR_MAX_TIMEOUT_SECONDS, type MonitorBatch, type MonitorController, type MonitorStopReason } from "./background-monitor.ts";
9
+ import { CARD_TONE, renderCard, type CardTheme } from "./shell-card.ts";
10
+ import { sanitizeTerminalText } from "./terminal-theme.ts";
11
+
12
+ export const JOB_NOTICE_TYPE = "gentle-jobs.notice";
13
+ export const JOB_GLYPH = "⧗";
14
+
15
+ export function formatJobDuration(ms: number): string {
16
+ const seconds = Math.max(0, Math.round(ms / 1000));
17
+ if (seconds < 60) return `${seconds}s`;
18
+ const minutes = Math.floor(seconds / 60);
19
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
20
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
21
+ }
22
+
23
+ const quoted = (job: JobRecord) => `${job.id} ("${job.label}")`;
24
+ const noun = (job: JobRecord) => (job.kind === "monitor" ? "Monitor" : "Background job");
25
+ const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? "" : "s"}`;
26
+
27
+ /** One-line outcome, shared by the notice, the list, and the jobs view. */
28
+ export function jobOutcome(job: JobRecord): string {
29
+ if (job.status === "running") return "running";
30
+ if (job.status === "stopped") return "stopped";
31
+ if (job.status === "failed") return `could not run: ${job.error ?? "unknown error"}`;
32
+ return job.exitCode === null || job.exitCode === undefined ? "exited without an exit code" : `exited with code ${job.exitCode}`;
33
+ }
34
+
35
+ /** Model-facing exit notice: everything needed to act without polling. */
36
+ export function jobNoticeText(job: JobRecord): string {
37
+ const duration = job.endedAt === undefined ? "" : ` after ${formatJobDuration(job.endedAt - job.startedAt)}`;
38
+ // Only a human stops a job without the agent knowing (job_stop sends no notice).
39
+ const outcome = job.status === "stopped" ? "was stopped by the user" : jobOutcome(job);
40
+ const events = job.kind === "monitor" ? `, ${plural(job.events ?? 0, "event")}` : "";
41
+ const lines = [`${noun(job)} ${quoted(job)} ${outcome}${duration}${events}.`, `Command: ${job.command}`];
42
+ if (job.tail.length > 0) lines.push("Last output:", ...job.tail);
43
+ else lines.push("No output.");
44
+ lines.push(`Full output: ${job.outputPath} (read it with the read tool if you need more).`);
45
+ return lines.join("\n");
46
+ }
47
+
48
+ /** Model-facing notice for one batch of monitor lines. */
49
+ export function monitorEventsText(job: JobRecord, batch: MonitorBatch): string {
50
+ const lines = [`Monitor ${quoted(job)} reported ${plural(batch.lines.length, "new line")}:`, ...batch.lines];
51
+ if (batch.omitted > 0) lines.push(`(${plural(batch.omitted, "more line")} not shown; full output in ${job.outputPath})`);
52
+ return lines.join("\n");
53
+ }
54
+
55
+ /** Model-facing notice for a monitor the harness stopped. */
56
+ export function monitorStoppedText(job: JobRecord, reason: MonitorStopReason): string {
57
+ const events = plural(job.events ?? 0, "event");
58
+ const output = `Full output: ${job.outputPath}`;
59
+ if (reason === "timeout") {
60
+ const limit = job.timeoutSeconds === undefined ? "its timeout" : `its ${job.timeoutSeconds}s timeout`;
61
+ return `Monitor ${quoted(job)} reached ${limit} after ${events} and was stopped. Start a new monitor if you still need to watch it.\n${output}`;
62
+ }
63
+ return `Monitor ${quoted(job)} was stopped: it printed more than ${MONITOR_FLOOD_LINES} lines within ${MONITOR_FLOOD_WINDOW_MS / 1000}s (${events}). Narrow the command's filter before watching it again.\n${output}`;
64
+ }
65
+
66
+ function listLine(job: JobRecord, now: number): string {
67
+ const ended = job.endedAt ?? now;
68
+ const status = `${job.kind === "monitor" ? "monitor " : ""}${job.status === "exited" ? `exited ${job.exitCode ?? "?"}` : job.status}`;
69
+ return `${job.id} · ${status} · ${job.label} · ${formatJobDuration(ended - job.startedAt)} · ${job.outputPath}`;
70
+ }
71
+
72
+ type ToolText = { content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> };
73
+ const text = (value: string, details: Record<string, unknown> = {}): ToolText => ({ content: [{ type: "text", text: value }], details });
74
+ export const jobDetails = (job: JobRecord, event?: "events" | MonitorStopReason) => ({ gentleJobs: { jobId: job.id, kind: job.kind, label: job.label, status: job.status, exitCode: job.exitCode ?? null, outputPath: job.outputPath, ...(event ? { event } : {}) } });
75
+
76
+ export interface BackgroundJobToolsOptions {
77
+ registry: JobRegistry;
78
+ /** The session the tool call runs in; jobs belong to it. */
79
+ sessionId(ctx: ExtensionContext): string;
80
+ now(): number;
81
+ /** Called after a job starts or stops, so views can refresh. */
82
+ onChange?(): void;
83
+ /** Starts and stops monitors; without it the monitor tool is not offered. */
84
+ monitors?: MonitorController;
85
+ }
86
+
87
+ export function registerBackgroundJobTools(pi: ExtensionAPI, options: BackgroundJobToolsOptions): void {
88
+ const { registry } = options;
89
+ const owned = (ctx: ExtensionContext, id: string) => {
90
+ const job = registry.get(id);
91
+ return job && job.ownerSessionId === options.sessionId(ctx) ? job : undefined;
92
+ };
93
+
94
+ pi.registerTool({
95
+ name: BASH_BACKGROUND_TOOL,
96
+ label: "Bash (background)",
97
+ description: [
98
+ "Run a shell command in the background and return a job id at once. You are notified once, automatically, when it exits (exit code, last output lines, output file); do not poll, sleep, or delegate a subagent to wait.",
99
+ "Use synchronous bash for local, finite, deterministic checks reasonably expected to finish within 30 seconds, with an explicit timeout <= 30 seconds (30 included). Use bash_background for longer or unknown duration, external waits, CI, builds, and servers, including deploys and long tests. Report a timeout as failure; never automatically retry, restart, or migrate it to background. Preserve RED/GREEN and public-check exit codes, stdout, and stderr; do not defer or batch Engram bugfix saves and session summaries or bypass RDD. This is guidance, not a performance guarantee.",
100
+ "Put the wait condition inside the command so its exit is the event, e.g. `gh run watch <run-id> --exit-status` or `until curl -sf localhost:3000/health; do sleep 1; done`.",
101
+ `Output goes to a file you can read with the read tool. Stop a job with job_stop; list jobs with job_list. At most ${MAX_RUNNING_JOBS} run at once.`,
102
+ ].join(" "),
103
+ parameters: {
104
+ type: "object",
105
+ additionalProperties: false,
106
+ required: ["command"],
107
+ properties: {
108
+ command: { type: "string", description: "Shell command to run in the background, in the session working directory." },
109
+ label: { type: "string", maxLength: 80, description: "Short human-readable label, e.g. 'CI for PR 1834'." },
110
+ },
111
+ } as never,
112
+ async execute(_id, params, _signal, _onUpdate, ctx) {
113
+ const input = params as { command?: unknown; label?: unknown };
114
+ if (typeof input.command !== "string" || !input.command.trim()) throw new Error("bash_background requires a non-empty command");
115
+ const job = registry.start({ command: input.command, cwd: ctx.cwd, ownerSessionId: options.sessionId(ctx), label: typeof input.label === "string" ? input.label : undefined });
116
+ options.onChange?.();
117
+ return text(
118
+ `Started background job ${quoted(job)}.\nOutput: ${job.outputPath}\nYou will be notified once when it exits; keep working or end your turn. Stop it with job_stop.`,
119
+ jobDetails(job),
120
+ );
121
+ },
122
+ });
123
+
124
+ const monitors = options.monitors;
125
+ if (monitors) pi.registerTool({
126
+ name: MONITOR_TOOL,
127
+ label: "Monitor",
128
+ description: [
129
+ "Run a command in the background and receive each output line as an event while it runs, without waiting for it to exit. Do not poll or sleep between events.",
130
+ "Use it to react as things happen: `tail -f app.log | grep --line-buffered ERROR`, or a loop that prints each CI check as it finishes. For a single result when a command ends, use bash_background instead.",
131
+ `Lines within ${MONITOR_BATCH_MS} ms arrive as one notice of at most ${MONITOR_LINES_PER_NOTICE} lines. timeout_seconds is mandatory (1-${MONITOR_MAX_TIMEOUT_SECONDS}); the monitor is stopped when it expires, and also when it prints more than ${MONITOR_FLOOD_LINES} lines within ${MONITOR_FLOOD_WINDOW_MS / 1000}s, so filter the command's output.`,
132
+ "Stop it with job_stop; list it with job_list.",
133
+ ].join(" "),
134
+ parameters: {
135
+ type: "object",
136
+ additionalProperties: false,
137
+ required: ["command", "timeout_seconds"],
138
+ properties: {
139
+ command: { type: "string", description: "Shell command whose output lines are events, run in the session working directory." },
140
+ timeout_seconds: { type: "integer", minimum: 1, maximum: MONITOR_MAX_TIMEOUT_SECONDS, description: "Mandatory: stop the monitor after this many seconds." },
141
+ label: { type: "string", maxLength: 80, description: "Short human-readable label, e.g. 'CI checks for PR 1842'." },
142
+ },
143
+ } as never,
144
+ async execute(_id, params, _signal, _onUpdate, ctx) {
145
+ const input = params as { command?: unknown; label?: unknown; timeout_seconds?: unknown };
146
+ if (typeof input.command !== "string" || !input.command.trim()) throw new Error("monitor requires a non-empty command");
147
+ const timeoutSeconds = typeof input.timeout_seconds === "number" ? input.timeout_seconds : Number.NaN;
148
+ const job = monitors.start({ command: input.command, cwd: ctx.cwd, ownerSessionId: options.sessionId(ctx), label: typeof input.label === "string" ? input.label : undefined, timeoutSeconds });
149
+ options.onChange?.();
150
+ return text(
151
+ `Started monitor ${quoted(job)} for up to ${timeoutSeconds}s.\nOutput: ${job.outputPath}\nEach new output line reaches you as an event; keep working or end your turn. Stop it with job_stop.`,
152
+ jobDetails(job),
153
+ );
154
+ },
155
+ });
156
+
157
+ pi.registerTool({
158
+ name: "job_stop",
159
+ label: "Stop background job",
160
+ description: "Stop a background job started with bash_background, killing its whole process tree. A stopped job sends no exit notice.",
161
+ parameters: { type: "object", additionalProperties: false, required: ["job_id"], properties: { job_id: { type: "string" } } } as never,
162
+ async execute(_id, params, _signal, _onUpdate, ctx) {
163
+ const id = (params as { job_id?: unknown }).job_id;
164
+ const job = typeof id === "string" ? owned(ctx, id) : undefined;
165
+ if (!job) throw new Error(`No background job ${String(id)} in this session. Use job_list to see this session's jobs.`);
166
+ const wasRunning = job.status === "running";
167
+ options.monitors?.cancel(job.id);
168
+ registry.stop(job.id);
169
+ options.onChange?.();
170
+ return text(wasRunning ? `Stopped ${quoted(job)}.` : `${quoted(job)} was already ${jobOutcome(job)}.`, jobDetails(job));
171
+ },
172
+ });
173
+
174
+ pi.registerTool({
175
+ name: "job_list",
176
+ label: "List background jobs",
177
+ description: "List this session's background jobs with status, label, elapsed time, and output file.",
178
+ parameters: { type: "object", additionalProperties: false, properties: {} } as never,
179
+ async execute(_id, _params, _signal, _onUpdate, ctx) {
180
+ const jobs = registry.list(options.sessionId(ctx));
181
+ if (jobs.length === 0) return text("No background jobs in this session.");
182
+ const now = options.now();
183
+ return text(jobs.map((job) => listLine(job, now)).join("\n"), { gentleJobs: { count: jobs.length } });
184
+ },
185
+ });
186
+
187
+ pi.registerMessageRenderer(JOB_NOTICE_TYPE, (message, renderOptions, theme) => {
188
+ const details = (message.details as { gentleJobs?: { label?: string; status?: string; exitCode?: number | null; kind?: string; event?: string } } | undefined)?.gentleJobs;
189
+ const content = typeof message.content === "string" ? message.content : message.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
190
+ const lines = sanitizeTerminalText(content).split("\n");
191
+ const tone = details?.event === "events" ? CARD_TONE.INFO
192
+ : details?.status === "exited" && details.exitCode === 0 ? CARD_TONE.SUCCESS : CARD_TONE.ERROR;
193
+ let hint: string;
194
+ try { hint = keyHint("app.tools.expand", renderOptions.expanded ? "collapse" : "expand"); } catch { hint = renderOptions.expanded ? "collapse" : "expand"; }
195
+ return {
196
+ render(width: number) {
197
+ return renderCard({ title: details?.kind === "monitor" ? "Monitor" : "Background job", subtitle: details?.label, body: lines, tone, glyph: JOB_GLYPH }, theme as CardTheme, width, { expanded: renderOptions.expanded, previewRows: 3, hint });
198
+ },
199
+ invalidate() {},
200
+ };
201
+ });
202
+ }
@@ -0,0 +1,253 @@
1
+ // Background jobs.
2
+ //
3
+ // A background job runs a shell command while the parent keeps working or
4
+ // stays idle, so waiting on CI, a build, or a server never costs a sleep loop
5
+ // or a subagent. A wait condition lives inside the command itself (for
6
+ // example `until grep -q ready log; do sleep 1; done` or
7
+ // `gh run watch <id> --exit-status`): the job's exit is the notification.
8
+ //
9
+ // The registry owns live jobs and reports each job's own exit exactly once
10
+ // through `onSettled`. A stopped job reports nothing: whoever stopped it
11
+ // already knows. Execution goes through Pi's own shell operations
12
+ // (`createLocalBashOperations`), so the configured shell, the command
13
+ // transport, process groups, and process-tree kill match native Bash.
14
+
15
+ import { createWriteStream, type WriteStream } from "node:fs";
16
+ import { join } from "node:path";
17
+
18
+ /** The background sibling of Pi's native `bash` tool. */
19
+ export const BASH_BACKGROUND_TOOL = "bash_background";
20
+ /** A background job whose output lines are events. */
21
+ export const MONITOR_TOOL = "monitor";
22
+ /**
23
+ * Tools whose `command` input runs in a shell. Command guards (confirmation,
24
+ * YOLO, child safety) must cover every one of them, or the background tool
25
+ * would bypass them.
26
+ */
27
+ export const SHELL_COMMAND_TOOLS: ReadonlySet<string> = new Set(["bash", BASH_BACKGROUND_TOOL, MONITOR_TOOL]);
28
+
29
+ const STANDALONE_SLEEP_SEGMENT = /^sleep\s+\d+(?:\.\d+)?[smhd]?$/;
30
+
31
+ /**
32
+ * True only for a command that does nothing but sleep: one or more literal
33
+ * `sleep <number>[smhd]` segments joined by `&&` or `;`. Wait conditions
34
+ * (`until ...; do sleep 1; done`), loops, pipes and sleeps mixed with real
35
+ * commands are never standalone (#1903).
36
+ */
37
+ export function isStandaloneSleep(command: string): boolean {
38
+ const segments = command.split(/&&|;/).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
39
+ return segments.length > 0 && segments.every((segment) => STANDALONE_SLEEP_SEGMENT.test(segment));
40
+ }
41
+
42
+ /** Lines kept for the exit notice and job listings. */
43
+ export const TAIL_LINES = 20;
44
+ /**
45
+ * Characters kept per tail line. Progress bars and minified output can print
46
+ * one endless line; only its newest characters are kept, after an ellipsis.
47
+ */
48
+ export const MAX_TAIL_LINE_CHARS = 2000;
49
+ /** Running jobs allowed at once across the process. */
50
+ export const MAX_RUNNING_JOBS = 25;
51
+
52
+ /** Collects the last TAIL_LINES lines of a stream of output chunks. */
53
+ export function createOutputTail() {
54
+ const complete: string[] = [];
55
+ let partial = "";
56
+ let partialTruncated = false;
57
+ const bound = (line: string, truncated: boolean) => {
58
+ const cut = line.length > MAX_TAIL_LINE_CHARS;
59
+ const kept = cut ? line.slice(-MAX_TAIL_LINE_CHARS) : line;
60
+ return cut || truncated ? `…${kept}` : kept;
61
+ };
62
+ const clean = (line: string) => (line.endsWith("\r") ? line.slice(0, -1) : line);
63
+ return {
64
+ push(text: string) {
65
+ const parts = (partial + text).split("\n");
66
+ partial = parts.pop() ?? "";
67
+ for (const [index, part] of parts.entries()) {
68
+ complete.push(bound(clean(part), index === 0 && partialTruncated));
69
+ if (complete.length > TAIL_LINES) complete.shift();
70
+ if (index === 0) partialTruncated = false;
71
+ }
72
+ if (partial.length > MAX_TAIL_LINE_CHARS) {
73
+ partial = partial.slice(-MAX_TAIL_LINE_CHARS);
74
+ partialTruncated = true;
75
+ }
76
+ },
77
+ lines(): string[] {
78
+ const all = partial.length > 0 ? [...complete, bound(clean(partial), partialTruncated)] : [...complete];
79
+ return all.slice(-TAIL_LINES);
80
+ },
81
+ };
82
+ }
83
+
84
+ /** The subset of Pi's `BashOperations` a job needs. */
85
+ export interface JobExecOperations {
86
+ exec(command: string, cwd: string, options: { onData: (data: Buffer) => void; signal?: AbortSignal }): Promise<{ exitCode: number | null }>;
87
+ }
88
+
89
+ export type JobStatus = "running" | "exited" | "stopped" | "failed";
90
+ /** A command reports its exit; a monitor also reports each output line. */
91
+ export type JobKind = "command" | "monitor";
92
+
93
+ export interface JobRecord {
94
+ readonly id: string;
95
+ readonly kind: JobKind;
96
+ readonly label: string;
97
+ readonly command: string;
98
+ readonly cwd: string;
99
+ readonly ownerSessionId: string;
100
+ readonly outputPath: string;
101
+ readonly startedAt: number;
102
+ status: JobStatus;
103
+ exitCode?: number | null;
104
+ error?: string;
105
+ endedAt?: number;
106
+ /** Last output lines; refreshed until the job ends. */
107
+ tail: string[];
108
+ /** Monitor only: output lines reported as events so far. */
109
+ events?: number;
110
+ /** Monitor only: the mandatory timeout it was started with. */
111
+ timeoutSeconds?: number;
112
+ }
113
+
114
+ export interface JobStartRequest {
115
+ command: string;
116
+ cwd: string;
117
+ ownerSessionId: string;
118
+ label?: string;
119
+ kind?: JobKind;
120
+ /** Called with each complete output line, and the last partial one at exit. */
121
+ onLine?: (line: string) => void;
122
+ }
123
+
124
+ export interface JobRegistryDeps {
125
+ /** Directory receiving one `<id>.log` file per job; read at each start. */
126
+ outputDir(): string;
127
+ now(): number;
128
+ /** Resolved at each start so a changed shell setting applies to the next job. */
129
+ shell(cwd: string): { operations: JobExecOperations; commandPrefix?: string };
130
+ /** Called once when a job ends on its own (exited or failed), never for a stop. */
131
+ onSettled(job: JobRecord): void;
132
+ }
133
+
134
+ export function createJobRegistry(deps: JobRegistryDeps) {
135
+ const jobs = new Map<string, JobRecord>();
136
+ const aborts = new Map<string, AbortController>();
137
+ // Every job's run, until its log file is closed and its exit reported.
138
+ const runs = new Set<Promise<void>>();
139
+ let counter = 0;
140
+
141
+ const running = () => [...jobs.values()].filter((job) => job.status === "running").length;
142
+
143
+ const finish = async (job: JobRecord, output: WriteStream, update: () => void) => {
144
+ update();
145
+ await new Promise<void>((resolve) => output.end(resolve));
146
+ aborts.delete(job.id);
147
+ };
148
+
149
+ const start = (request: JobStartRequest): JobRecord => {
150
+ if (running() >= MAX_RUNNING_JOBS) {
151
+ throw new Error(`${MAX_RUNNING_JOBS} background jobs are already running; stop one with job_stop first.`);
152
+ }
153
+ const { operations, commandPrefix } = deps.shell(request.cwd);
154
+ const id = `job-${++counter}`;
155
+ const job: JobRecord = {
156
+ id,
157
+ kind: request.kind ?? "command",
158
+ label: request.label?.trim() || request.command,
159
+ command: request.command,
160
+ cwd: request.cwd,
161
+ ownerSessionId: request.ownerSessionId,
162
+ outputPath: join(deps.outputDir(), `${id}.log`),
163
+ startedAt: deps.now(),
164
+ status: "running",
165
+ tail: [],
166
+ };
167
+ const output = createWriteStream(job.outputPath);
168
+ output.on("error", () => { /* A lost log must not crash the session; the tail survives. */ });
169
+ const tail = createOutputTail();
170
+ // Streaming decode keeps a multibyte character split across chunks intact.
171
+ const decoder = new TextDecoder("utf-8");
172
+ const abort = new AbortController();
173
+ jobs.set(id, job);
174
+ aborts.set(id, abort);
175
+ const execCommand = commandPrefix ? `${commandPrefix}\n${request.command}` : request.command;
176
+ let linePartial = "";
177
+ const emitLines = (text: string, end: boolean) => {
178
+ if (!request.onLine) return;
179
+ const parts = (linePartial + text).split("\n");
180
+ linePartial = end ? "" : parts.pop() ?? "";
181
+ // Keep one extra character so a cut partial still reads as cut.
182
+ if (linePartial.length > MAX_TAIL_LINE_CHARS) linePartial = linePartial.slice(-(MAX_TAIL_LINE_CHARS + 1));
183
+ for (const part of parts) {
184
+ const line = part.endsWith("\r") ? part.slice(0, -1) : part;
185
+ request.onLine(line.length > MAX_TAIL_LINE_CHARS ? `…${line.slice(-MAX_TAIL_LINE_CHARS)}` : line);
186
+ }
187
+ };
188
+ const flushDecoder = () => {
189
+ const rest = decoder.decode();
190
+ if (rest) {
191
+ tail.push(rest);
192
+ job.tail = tail.lines();
193
+ }
194
+ emitLines(rest, true);
195
+ };
196
+ const onData = (data: Buffer) => {
197
+ output.write(data);
198
+ const text = decoder.decode(data, { stream: true });
199
+ tail.push(text);
200
+ job.tail = tail.lines();
201
+ emitLines(text, false);
202
+ };
203
+ const run = operations.exec(execCommand, request.cwd, { onData, signal: abort.signal }).then(
204
+ ({ exitCode }) => finish(job, output, () => {
205
+ flushDecoder();
206
+ if (job.status !== "running") return;
207
+ job.status = "exited";
208
+ job.exitCode = exitCode;
209
+ job.endedAt = deps.now();
210
+ }),
211
+ (error: unknown) => finish(job, output, () => {
212
+ flushDecoder();
213
+ if (job.status !== "running") return;
214
+ job.status = "failed";
215
+ job.error = error instanceof Error ? error.message : String(error);
216
+ job.endedAt = deps.now();
217
+ }),
218
+ ).then(() => {
219
+ if (job.status === "exited" || job.status === "failed") deps.onSettled(job);
220
+ }).finally(() => runs.delete(run));
221
+ runs.add(run);
222
+ return job;
223
+ };
224
+
225
+ const stop = (id: string): JobRecord | undefined => {
226
+ const job = jobs.get(id);
227
+ if (!job) return undefined;
228
+ if (job.status === "running") {
229
+ job.status = "stopped";
230
+ job.endedAt = deps.now();
231
+ aborts.get(id)?.abort();
232
+ }
233
+ return job;
234
+ };
235
+
236
+ return {
237
+ start,
238
+ stop,
239
+ get: (id: string) => jobs.get(id),
240
+ /** Jobs in start order, optionally only those one session owns. */
241
+ list: (ownerSessionId?: string) => [...jobs.values()].filter((job) => ownerSessionId === undefined || job.ownerSessionId === ownerSessionId),
242
+ running,
243
+ /** Runs not yet finished: still executing or closing their log. */
244
+ pending: () => runs.size,
245
+ /** Stops every running job and resolves once every log file is closed. */
246
+ stopAll: async () => {
247
+ for (const id of jobs.keys()) stop(id);
248
+ await Promise.allSettled([...runs]);
249
+ },
250
+ };
251
+ }
252
+
253
+ export type JobRegistry = ReturnType<typeof createJobRegistry>;
@@ -0,0 +1,154 @@
1
+ // Background monitor: pure event batching.
2
+ //
3
+ // A monitor is a background job whose every stdout line is an event for the
4
+ // agent, delivered while the command keeps running (for example
5
+ // `tail -f app.log | grep --line-buffered ERROR`, or a loop that prints each
6
+ // CI check as it finishes). Lines arriving close together share one notice,
7
+ // each notice is bounded, and a stream that floods is stopped instead of
8
+ // burning the agent's turns. This module owns no timers or processes: the
9
+ // caller flushes MONITOR_BATCH_MS after a line opens a batch. The controller
10
+ // below wires it to a job registry with an injected scheduler.
11
+
12
+ import type { JobRecord, JobRegistry, JobStartRequest } from "./background-jobs.ts";
13
+
14
+ /** Lines arriving within this window share one notice. */
15
+ export const MONITOR_BATCH_MS = 200;
16
+ /** Lines kept per notice; the rest are only counted. */
17
+ export const MONITOR_LINES_PER_NOTICE = 20;
18
+ /** More lines than this within MONITOR_FLOOD_WINDOW_MS is a flood. */
19
+ export const MONITOR_FLOOD_LINES = 120;
20
+ export const MONITOR_FLOOD_WINDOW_MS = 60_000;
21
+ /** The mandatory monitor timeout is capped at 30 minutes. */
22
+ export const MONITOR_MAX_TIMEOUT_SECONDS = 1800;
23
+
24
+ export interface MonitorBatch {
25
+ lines: string[];
26
+ /** Lines beyond MONITOR_LINES_PER_NOTICE, counted but not delivered. */
27
+ omitted: number;
28
+ }
29
+
30
+ export function createEventBatcher() {
31
+ let batch: MonitorBatch | undefined;
32
+ let total = 0;
33
+ // Arrival times inside the current flood window, oldest first.
34
+ const recent: number[] = [];
35
+ return {
36
+ /**
37
+ * Records one stdout line. `opensBatch` asks the caller to schedule a
38
+ * flush; `flood` asks it to stop the monitor.
39
+ */
40
+ pushLine(line: string, now: number): { opensBatch: boolean; flood: boolean } {
41
+ if (line.trim().length === 0) return { opensBatch: false, flood: false };
42
+ total += 1;
43
+ recent.push(now);
44
+ while (recent.length > 0 && now - recent[0]! >= MONITOR_FLOOD_WINDOW_MS) recent.shift();
45
+ const opensBatch = batch === undefined;
46
+ batch ??= { lines: [], omitted: 0 };
47
+ if (batch.lines.length < MONITOR_LINES_PER_NOTICE) batch.lines.push(line);
48
+ else batch.omitted += 1;
49
+ return { opensBatch, flood: recent.length > MONITOR_FLOOD_LINES };
50
+ },
51
+ /** Takes the current batch, or undefined when no line arrived. */
52
+ flush(): MonitorBatch | undefined {
53
+ const taken = batch;
54
+ batch = undefined;
55
+ return taken;
56
+ },
57
+ /** Every event line seen so far. */
58
+ total: () => total,
59
+ };
60
+ }
61
+
62
+ /** Coalesces two pending notices for one monitor within the same line bound. */
63
+ export function mergeMonitorBatches(first: MonitorBatch, second: MonitorBatch): MonitorBatch {
64
+ const room = Math.max(0, MONITOR_LINES_PER_NOTICE - first.lines.length);
65
+ return {
66
+ lines: [...first.lines, ...second.lines.slice(0, room)],
67
+ omitted: first.omitted + second.omitted + Math.max(0, second.lines.length - room),
68
+ };
69
+ }
70
+
71
+ export type MonitorStopReason = "timeout" | "flood";
72
+
73
+ export type MonitorNotice =
74
+ | { job: JobRecord; kind: "events"; batch: MonitorBatch }
75
+ | { job: JobRecord; kind: "stopped"; reason: MonitorStopReason };
76
+
77
+ export interface MonitorControllerDeps {
78
+ registry: JobRegistry;
79
+ schedule(fn: () => void, ms: number): () => void;
80
+ now(): number;
81
+ /** Hands one notice to the parent delivery router. */
82
+ deliver(notice: MonitorNotice): void;
83
+ }
84
+
85
+ export interface MonitorStartRequest extends Omit<JobStartRequest, "kind" | "onLine"> {
86
+ /** Mandatory: the monitor is stopped after this many seconds (1-1800). */
87
+ timeoutSeconds: number;
88
+ }
89
+
90
+ export function createMonitorController(deps: MonitorControllerDeps) {
91
+ const active = new Map<string, { batcher: ReturnType<typeof createEventBatcher>; cancelFlush?: () => void; cancelTimeout: () => void }>();
92
+
93
+ const flush = (job: JobRecord) => {
94
+ const state = active.get(job.id);
95
+ if (!state) return;
96
+ state.cancelFlush?.();
97
+ state.cancelFlush = undefined;
98
+ const batch = state.batcher.flush();
99
+ if (batch) deps.deliver({ job, kind: "events", batch });
100
+ };
101
+ const cancel = (id: string) => {
102
+ const state = active.get(id);
103
+ if (!state) return;
104
+ state.cancelFlush?.();
105
+ state.cancelTimeout();
106
+ active.delete(id);
107
+ };
108
+ const stopFor = (job: JobRecord, reason: MonitorStopReason) => {
109
+ // A command that already ended reports its own exit (finish flushes it).
110
+ if (job.status !== "running") return;
111
+ flush(job);
112
+ cancel(job.id);
113
+ deps.registry.stop(job.id);
114
+ deps.deliver({ job, kind: "stopped", reason });
115
+ };
116
+
117
+ return {
118
+ start(request: MonitorStartRequest): JobRecord {
119
+ const { timeoutSeconds, ...rest } = request;
120
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > MONITOR_MAX_TIMEOUT_SECONDS) {
121
+ throw new Error(`timeout_seconds must be a whole number from 1 to ${MONITOR_MAX_TIMEOUT_SECONDS}.`);
122
+ }
123
+ const batcher = createEventBatcher();
124
+ let job: JobRecord | undefined;
125
+ const onLine = (line: string) => {
126
+ const state = job && active.get(job.id);
127
+ // A stopped or finished monitor reports nothing more.
128
+ if (!job || !state || job.status !== "running") return;
129
+ const { opensBatch, flood } = batcher.pushLine(line, deps.now());
130
+ job.events = batcher.total();
131
+ if (flood) return stopFor(job, "flood");
132
+ if (opensBatch) state.cancelFlush = deps.schedule(() => job && flush(job), MONITOR_BATCH_MS);
133
+ };
134
+ job = deps.registry.start({ ...rest, kind: "monitor", onLine });
135
+ job.events = 0;
136
+ job.timeoutSeconds = timeoutSeconds;
137
+ const started = job;
138
+ active.set(started.id, { batcher, cancelTimeout: deps.schedule(() => stopFor(started, "timeout"), timeoutSeconds * 1000) });
139
+ return started;
140
+ },
141
+ /** The job ended on its own: deliver its pending lines before the exit notice. */
142
+ finish(job: JobRecord) {
143
+ flush(job);
144
+ cancel(job.id);
145
+ },
146
+ /** Stopped by job_stop or a human: drop its timers and pending lines. */
147
+ cancel,
148
+ cancelAll() {
149
+ for (const id of [...active.keys()]) cancel(id);
150
+ },
151
+ };
152
+ }
153
+
154
+ export type MonitorController = ReturnType<typeof createMonitorController>;