gentle-pi 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,227 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
7
+ import { createJobRegistry, createOutputTail, MAX_RUNNING_JOBS, MAX_TAIL_LINE_CHARS, TAIL_LINES, type JobExecOperations, type JobRecord } from "../lib/background-jobs.ts";
8
+
9
+ // Background jobs run a shell command while the parent keeps working or stays
10
+ // idle; the registry reports each job's own exit exactly once.
11
+
12
+ test("output tail keeps the last TAIL_LINES lines, joins partial chunks, and strips CR", () => {
13
+ const tail = createOutputTail();
14
+ const lines = Array.from({ length: TAIL_LINES + 5 }, (_, i) => `line ${i}`);
15
+ tail.push(`${lines.join("\r\n")}\r\n`);
16
+ assert.deepEqual(tail.lines(), lines.slice(5));
17
+ tail.push("par");
18
+ tail.push("tial");
19
+ assert.deepEqual(tail.lines().at(-1), "partial");
20
+ assert.equal(tail.lines().length, TAIL_LINES);
21
+ });
22
+
23
+ test("output tail bounds a line that never ends, keeping its newest characters", () => {
24
+ const tail = createOutputTail();
25
+ for (let i = 0; i < 50; i++) tail.push(`${"x".repeat(MAX_TAIL_LINE_CHARS)}`);
26
+ tail.push("END");
27
+ const [line] = tail.lines();
28
+ assert.ok(line!.length <= MAX_TAIL_LINE_CHARS + 1, `kept ${line!.length} characters`);
29
+ assert.ok(line!.startsWith("…") && line!.endsWith("xEND"));
30
+ tail.push(`\n${"y".repeat(MAX_TAIL_LINE_CHARS * 3)}\nshort\n`);
31
+ assert.ok(tail.lines().every((entry) => entry.length <= MAX_TAIL_LINE_CHARS + 1));
32
+ assert.equal(tail.lines().at(-1), "short");
33
+ });
34
+
35
+ interface FakeRun {
36
+ command: string;
37
+ cwd: string;
38
+ onData: (data: Buffer) => void;
39
+ signal?: AbortSignal;
40
+ resolve: (exitCode: number | null) => void;
41
+ reject: (error: Error) => void;
42
+ }
43
+
44
+ function fakeOperations() {
45
+ const runs: FakeRun[] = [];
46
+ const operations: JobExecOperations = {
47
+ exec: (command, cwd, options) => new Promise((resolve, reject) => {
48
+ const run: FakeRun = { command, cwd, onData: options.onData, signal: options.signal, resolve: (exitCode) => resolve({ exitCode }), reject };
49
+ options.signal?.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
50
+ runs.push(run);
51
+ }),
52
+ };
53
+ return { runs, operations };
54
+ }
55
+
56
+ function setup(commandPrefix?: string) {
57
+ const dir = mkdtempSync(join(tmpdir(), "gentle-jobs-test-"));
58
+ const fake = fakeOperations();
59
+ const settled: JobRecord[] = [];
60
+ let clock = 1000;
61
+ const registry = createJobRegistry({
62
+ outputDir: () => dir,
63
+ now: () => clock,
64
+ shell: () => ({ operations: fake.operations, commandPrefix }),
65
+ onSettled: (job) => settled.push(job),
66
+ });
67
+ const tick = (ms: number) => { clock += ms; };
68
+ return { dir, fake, settled, registry, tick, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
69
+ }
70
+
71
+ // Settling closes the job's log file first, which takes real I/O turns.
72
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
73
+
74
+ test("start returns a running job at once and its exit settles it exactly once with output on disk", async () => {
75
+ const { fake, settled, registry, tick, cleanup } = setup();
76
+ try {
77
+ const job = registry.start({ command: "make ci", cwd: "/repo", label: "ci", ownerSessionId: "s1" });
78
+ assert.equal(job.status, "running");
79
+ assert.equal(job.id, "job-1");
80
+ assert.equal(fake.runs[0]!.command, "make ci");
81
+ assert.equal(fake.runs[0]!.cwd, "/repo");
82
+ fake.runs[0]!.onData(Buffer.from("building\nok\n"));
83
+ tick(4200);
84
+ fake.runs[0]!.resolve(2);
85
+ await settle();
86
+ assert.equal(settled.length, 1);
87
+ const done = settled[0]!;
88
+ assert.equal(done.status, "exited");
89
+ assert.equal(done.exitCode, 2);
90
+ assert.equal(done.endedAt! - done.startedAt, 4200);
91
+ assert.deepEqual(done.tail, ["building", "ok"]);
92
+ assert.equal(readFileSync(done.outputPath, "utf8"), "building\nok\n");
93
+ assert.equal(registry.get("job-1")?.status, "exited");
94
+ } finally { cleanup(); }
95
+ });
96
+
97
+ test("a multibyte character split across output chunks reaches the tail intact", async () => {
98
+ const { fake, settled, registry, cleanup } = setup();
99
+ try {
100
+ registry.start({ command: "echo", cwd: "/repo", ownerSessionId: "s1" });
101
+ const bytes = Buffer.from("ñandú listo\n");
102
+ fake.runs[0]!.onData(bytes.subarray(0, 1));
103
+ fake.runs[0]!.onData(bytes.subarray(1, 5));
104
+ fake.runs[0]!.onData(bytes.subarray(5));
105
+ fake.runs[0]!.resolve(0);
106
+ await settle();
107
+ assert.deepEqual(settled[0]!.tail, ["ñandú listo"]);
108
+ } finally { cleanup(); }
109
+ });
110
+
111
+ test("onLine receives each complete output line across chunks, and the last partial line at exit", async () => {
112
+ const { fake, registry, cleanup } = setup();
113
+ try {
114
+ const lines: string[] = [];
115
+ const job = registry.start({ command: "watch", cwd: "/repo", ownerSessionId: "s1", kind: "monitor", onLine: (line) => lines.push(line) });
116
+ assert.equal(job.kind, "monitor");
117
+ fake.runs[0]!.onData(Buffer.from("check lint: pa"));
118
+ fake.runs[0]!.onData(Buffer.from("ss\r\ncheck test: fail\nlast"));
119
+ assert.deepEqual(lines, ["check lint: pass", "check test: fail"]);
120
+ fake.runs[0]!.resolve(0);
121
+ await settle();
122
+ assert.deepEqual(lines, ["check lint: pass", "check test: fail", "last"]);
123
+ assert.equal(registry.start({ command: "x", cwd: "/repo", ownerSessionId: "s1" }).kind, "command");
124
+ } finally { cleanup(); }
125
+ });
126
+
127
+ test("onLine bounds an endless line to its newest MAX_TAIL_LINE_CHARS characters", async () => {
128
+ const { fake, registry, cleanup } = setup();
129
+ try {
130
+ const lines: string[] = [];
131
+ registry.start({ command: "watch", cwd: "/repo", ownerSessionId: "s1", kind: "monitor", onLine: (line) => lines.push(line) });
132
+ fake.runs[0]!.onData(Buffer.from(`${"x".repeat(MAX_TAIL_LINE_CHARS * 5)}END\nshort\n`));
133
+ assert.equal(lines.length, 2);
134
+ assert.ok(lines[0]!.length <= MAX_TAIL_LINE_CHARS + 1 && lines[0]!.startsWith("…") && lines[0]!.endsWith("xEND"));
135
+ assert.equal(lines[1], "short");
136
+ fake.runs[0]!.onData(Buffer.from("y".repeat(MAX_TAIL_LINE_CHARS * 5)));
137
+ fake.runs[0]!.onData(Buffer.from("TAIL\n"));
138
+ assert.ok(lines[2]!.length <= MAX_TAIL_LINE_CHARS + 1 && lines[2]!.startsWith("…") && lines[2]!.endsWith("yTAIL"));
139
+ } finally { cleanup(); }
140
+ });
141
+
142
+ test("stopAll resolves only after every job's log file is closed", async () => {
143
+ const { registry, cleanup } = setup();
144
+ try {
145
+ const job = registry.start({ command: "sleep 100", cwd: "/repo", ownerSessionId: "s1" });
146
+ await registry.stopAll();
147
+ assert.equal(job.status, "stopped");
148
+ assert.equal(registry.pending(), 0);
149
+ } finally { cleanup(); }
150
+ });
151
+
152
+ test("the shell command prefix runs before the command, but the job keeps the original command", () => {
153
+ const { fake, registry, cleanup } = setup("shopt -s expand_aliases");
154
+ try {
155
+ const job = registry.start({ command: "ll", cwd: "/repo", ownerSessionId: "s1" });
156
+ assert.equal(fake.runs[0]!.command, "shopt -s expand_aliases\nll");
157
+ assert.equal(job.command, "ll");
158
+ assert.equal(job.label, "ll");
159
+ } finally { cleanup(); }
160
+ });
161
+
162
+ test("stop aborts a running job and sends no exit notice; stopping again or an unknown id is a no-op", async () => {
163
+ const { fake, settled, registry, cleanup } = setup();
164
+ try {
165
+ registry.start({ command: "sleep 100", cwd: "/repo", ownerSessionId: "s1" });
166
+ const stopped = registry.stop("job-1");
167
+ assert.equal(stopped?.status, "stopped");
168
+ assert.equal(fake.runs[0]!.signal?.aborted, true);
169
+ await settle();
170
+ assert.equal(settled.length, 0);
171
+ assert.equal(registry.get("job-1")?.status, "stopped");
172
+ assert.equal(registry.stop("job-1")?.status, "stopped");
173
+ assert.equal(registry.stop("job-404"), undefined);
174
+ } finally { cleanup(); }
175
+ });
176
+
177
+ test("a job that fails to run settles as failed with the error", async () => {
178
+ const { fake, settled, registry, cleanup } = setup();
179
+ try {
180
+ registry.start({ command: "x", cwd: "/missing", ownerSessionId: "s1" });
181
+ fake.runs[0]!.reject(new Error("Working directory does not exist: /missing"));
182
+ await settle();
183
+ assert.equal(settled[0]?.status, "failed");
184
+ assert.match(settled[0]!.error!, /Working directory does not exist/);
185
+ } finally { cleanup(); }
186
+ });
187
+
188
+ test("at most MAX_RUNNING_JOBS run at once; a finished job frees its slot", async () => {
189
+ const { fake, registry, cleanup } = setup();
190
+ try {
191
+ for (let i = 0; i < MAX_RUNNING_JOBS; i++) registry.start({ command: `c${i}`, cwd: "/repo", ownerSessionId: "s1" });
192
+ assert.throws(() => registry.start({ command: "one more", cwd: "/repo", ownerSessionId: "s1" }), /25 background jobs are already running/);
193
+ assert.equal(fake.runs.length, MAX_RUNNING_JOBS);
194
+ fake.runs[0]!.resolve(0);
195
+ await settle();
196
+ assert.equal(registry.start({ command: "one more", cwd: "/repo", ownerSessionId: "s1" }).status, "running");
197
+ } finally { cleanup(); }
198
+ });
199
+
200
+ test("list filters by owner session and stopAll stops every running job without notices", async () => {
201
+ const { settled, registry, cleanup } = setup();
202
+ try {
203
+ registry.start({ command: "a", cwd: "/repo", ownerSessionId: "s1" });
204
+ registry.start({ command: "b", cwd: "/repo", ownerSessionId: "s2" });
205
+ assert.deepEqual(registry.list("s1").map((job) => job.command), ["a"]);
206
+ assert.equal(registry.list().length, 2);
207
+ registry.stopAll();
208
+ await settle();
209
+ assert.deepEqual(registry.list().map((job) => job.status), ["stopped", "stopped"]);
210
+ assert.equal(settled.length, 0);
211
+ } finally { cleanup(); }
212
+ });
213
+
214
+ test("a real shell job runs through Pi's bash operations and reports its exit code", { skip: process.platform === "win32" }, async () => {
215
+ const dir = mkdtempSync(join(tmpdir(), "gentle-jobs-real-"));
216
+ try {
217
+ const done = new Promise<JobRecord>((resolve) => {
218
+ const registry = createJobRegistry({ outputDir: () => dir, now: Date.now, shell: () => ({ operations: createLocalBashOperations() }), onSettled: resolve });
219
+ registry.start({ command: "echo one; echo two >&2; exit 3", cwd: dir, ownerSessionId: "s1" });
220
+ });
221
+ const job = await done;
222
+ assert.equal(job.status, "exited");
223
+ assert.equal(job.exitCode, 3);
224
+ assert.deepEqual([...job.tail].sort(), ["one", "two"]);
225
+ assert.deepEqual(readFileSync(job.outputPath, "utf8").split("\n").filter(Boolean).sort(), ["one", "two"]);
226
+ } finally { rmSync(dir, { recursive: true, force: true }); }
227
+ });
@@ -0,0 +1,195 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ createEventBatcher, MONITOR_FLOOD_LINES, MONITOR_FLOOD_WINDOW_MS, MONITOR_LINES_PER_NOTICE, mergeMonitorBatches,
5
+ } from "../lib/background-monitor.ts";
6
+
7
+ // The batcher turns a monitor's stdout lines into bounded notices. It owns no
8
+ // timers: the caller flushes MONITOR_BATCH_MS after a line opens a batch.
9
+
10
+ test("the first line of a batch asks the caller to schedule a flush; later lines join it", () => {
11
+ const batcher = createEventBatcher();
12
+ assert.deepEqual(batcher.pushLine("check lint: pass", 0), { opensBatch: true, flood: false });
13
+ assert.deepEqual(batcher.pushLine("check test: fail", 50), { opensBatch: false, flood: false });
14
+ assert.deepEqual(batcher.flush(), { lines: ["check lint: pass", "check test: fail"], omitted: 0 });
15
+ assert.equal(batcher.flush(), undefined, "an empty batch is never delivered");
16
+ assert.equal(batcher.pushLine("check e2e: pass", 300).opensBatch, true, "after a flush the next line opens a new batch");
17
+ assert.equal(batcher.total(), 3);
18
+ });
19
+
20
+ test("a batch keeps at most MONITOR_LINES_PER_NOTICE lines and counts the rest", () => {
21
+ const batcher = createEventBatcher();
22
+ for (let i = 0; i < MONITOR_LINES_PER_NOTICE + 7; i++) batcher.pushLine(`line ${i}`, i);
23
+ const batch = batcher.flush()!;
24
+ assert.equal(batch.lines.length, MONITOR_LINES_PER_NOTICE);
25
+ assert.equal(batch.lines[0], "line 0");
26
+ assert.equal(batch.omitted, 7);
27
+ assert.equal(batcher.total(), MONITOR_LINES_PER_NOTICE + 7);
28
+ });
29
+
30
+ test("more than MONITOR_FLOOD_LINES lines within the flood window is a flood; a slower stream is not", () => {
31
+ const slow = createEventBatcher();
32
+ const spacing = Math.ceil(MONITOR_FLOOD_WINDOW_MS / MONITOR_FLOOD_LINES) + 1;
33
+ for (let i = 0; i < MONITOR_FLOOD_LINES * 3; i++) assert.equal(slow.pushLine("tick", i * spacing).flood, false);
34
+ const fast = createEventBatcher();
35
+ for (let i = 0; i < MONITOR_FLOOD_LINES; i++) assert.equal(fast.pushLine("spam", i).flood, false);
36
+ assert.equal(fast.pushLine("spam", MONITOR_FLOOD_LINES).flood, true);
37
+ });
38
+
39
+ test("blank lines are not events", () => {
40
+ const batcher = createEventBatcher();
41
+ assert.deepEqual(batcher.pushLine(" ", 0), { opensBatch: false, flood: false });
42
+ assert.equal(batcher.total(), 0);
43
+ assert.equal(batcher.flush(), undefined);
44
+ });
45
+
46
+ test("pending batches for one monitor coalesce within the same line bound", () => {
47
+ assert.deepEqual(mergeMonitorBatches({ lines: ["a", "b"], omitted: 1 }, { lines: ["c"], omitted: 2 }), { lines: ["a", "b", "c"], omitted: 3 });
48
+ const full = { lines: Array.from({ length: MONITOR_LINES_PER_NOTICE }, (_, i) => `x${i}`), omitted: 0 };
49
+ assert.deepEqual(mergeMonitorBatches(full, { lines: ["late", "later"], omitted: 4 }), { lines: full.lines, omitted: 6 });
50
+ });
51
+
52
+ import { mkdtempSync, rmSync } from "node:fs";
53
+ import { tmpdir } from "node:os";
54
+ import { join } from "node:path";
55
+ import { createJobRegistry, type JobExecOperations, type JobRecord } from "../lib/background-jobs.ts";
56
+ import { createMonitorController, MONITOR_BATCH_MS, type MonitorNotice } from "../lib/background-monitor.ts";
57
+
58
+ // A fake clock and scheduler drive the controller; a fake shell feeds lines.
59
+ function monitorHarness() {
60
+ const dir = mkdtempSync(join(tmpdir(), "gentle-monitor-test-"));
61
+ let clock = 0;
62
+ const timers: Array<{ at: number; fn: () => void; live: boolean }> = [];
63
+ const schedule = (fn: () => void, ms: number) => {
64
+ const timer = { at: clock + ms, fn, live: true };
65
+ timers.push(timer);
66
+ return () => { timer.live = false; };
67
+ };
68
+ const advance = (ms: number) => {
69
+ clock += ms;
70
+ for (const timer of timers.filter((entry) => entry.live && entry.at <= clock)) { timer.live = false; timer.fn(); }
71
+ };
72
+ const runs: Array<{ onData(data: Buffer): void; signal?: AbortSignal; exit(code: number): void }> = [];
73
+ const operations: JobExecOperations = {
74
+ exec: (_command, _cwd, options) => new Promise((done, fail) => {
75
+ options.signal?.addEventListener("abort", () => fail(new Error("aborted")), { once: true });
76
+ runs.push({ onData: options.onData, signal: options.signal, exit: (code) => done({ exitCode: code }) });
77
+ }),
78
+ };
79
+ const settled: JobRecord[] = [];
80
+ let notifySettled!: () => void;
81
+ const completion = new Promise<void>((resolve) => { notifySettled = resolve; });
82
+ const registry = createJobRegistry({ outputDir: () => dir, now: () => clock, shell: () => ({ operations }), onSettled: (job) => { settled.push(job); notifySettled(); } });
83
+ const notices: MonitorNotice[] = [];
84
+ const controller = createMonitorController({ registry, schedule, now: () => clock, deliver: (notice) => notices.push(notice) });
85
+ const line = (text: string) => runs.at(-1)!.onData(Buffer.from(`${text}\n`));
86
+ return { controller, registry, runs, notices, settled, completion, advance, line, cleanup: () => rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }) };
87
+ }
88
+
89
+ test("a monitor delivers lines within one batch window as one events notice, and keeps running", () => {
90
+ const h = monitorHarness();
91
+ try {
92
+ const job = h.controller.start({ command: "gh run watch", cwd: "/repo", ownerSessionId: "s1", label: "CI", timeoutSeconds: 600 });
93
+ assert.equal(job.kind, "monitor");
94
+ h.line("lint: pass");
95
+ h.advance(50);
96
+ h.line("unit: fail");
97
+ assert.equal(h.notices.length, 0, "nothing is delivered before the batch window closes");
98
+ h.advance(MONITOR_BATCH_MS);
99
+ assert.deepEqual(h.notices, [{ job, kind: "events", batch: { lines: ["lint: pass", "unit: fail"], omitted: 0 } }]);
100
+ assert.equal(job.status, "running");
101
+ assert.equal(job.events, 2);
102
+ } finally { h.controller.cancelAll(); h.cleanup(); }
103
+ });
104
+
105
+ test("the mandatory timeout stops a monitor after delivering its pending lines", () => {
106
+ const h = monitorHarness();
107
+ try {
108
+ const job = h.controller.start({ command: "tail -f log", cwd: "/repo", ownerSessionId: "s1", timeoutSeconds: 30 });
109
+ h.advance(29_900);
110
+ h.line("ERROR one");
111
+ h.advance(100);
112
+ assert.deepEqual(h.notices.map((notice) => notice.kind), ["events", "stopped"]);
113
+ assert.deepEqual(h.notices[1], { job, kind: "stopped", reason: "timeout" });
114
+ assert.equal(job.status, "stopped");
115
+ assert.equal(h.runs[0]!.signal?.aborted, true);
116
+ } finally { h.controller.cancelAll(); h.cleanup(); }
117
+ });
118
+
119
+ test("a flooding monitor is stopped and reported once; later lines are ignored", () => {
120
+ const h = monitorHarness();
121
+ try {
122
+ const job = h.controller.start({ command: "yes", cwd: "/repo", ownerSessionId: "s1", timeoutSeconds: 600 });
123
+ for (let i = 0; i < 200; i++) h.line(`spam ${i}`);
124
+ const kinds = h.notices.map((notice) => notice.kind);
125
+ assert.deepEqual(kinds, ["events", "stopped"]);
126
+ assert.deepEqual(h.notices[1], { job, kind: "stopped", reason: "flood" });
127
+ assert.equal(job.status, "stopped");
128
+ h.advance(MONITOR_BATCH_MS * 10);
129
+ assert.equal(h.notices.length, 2, "a stopped monitor delivers nothing more");
130
+ } finally { h.controller.cancelAll(); h.cleanup(); }
131
+ });
132
+
133
+ test("finish flushes pending lines before the exit notice; cancel drops timers without notices", async () => {
134
+ const h = monitorHarness();
135
+ try {
136
+ const job = h.controller.start({ command: "watch", cwd: "/repo", ownerSessionId: "s1", timeoutSeconds: 60 });
137
+ h.line("last check: pass");
138
+ h.runs[0]!.exit(0);
139
+ await h.completion;
140
+ assert.equal(h.settled[0], job);
141
+ h.controller.finish(job);
142
+ assert.deepEqual(h.notices.map((notice) => notice.kind), ["events"]);
143
+ h.advance(120_000);
144
+ assert.equal(h.notices.length, 1, "a finished monitor has no timeout left");
145
+
146
+ const other = h.controller.start({ command: "watch", cwd: "/repo", ownerSessionId: "s1", timeoutSeconds: 60 });
147
+ h.line("pending");
148
+ h.controller.cancel(other.id);
149
+ h.advance(120_000);
150
+ assert.equal(h.notices.length, 1, "a cancelled monitor delivers nothing more");
151
+ } finally { h.controller.cancelAll(); h.cleanup(); }
152
+ });
153
+
154
+ test("timeout_seconds is mandatory and capped at 30 minutes", () => {
155
+ const h = monitorHarness();
156
+ try {
157
+ for (const timeoutSeconds of [0, -1, 1801, Number.NaN, 1.5]) {
158
+ assert.throws(() => h.controller.start({ command: "x", cwd: "/repo", ownerSessionId: "s1", timeoutSeconds }), /timeout_seconds must be a whole number from 1 to 1800/);
159
+ }
160
+ assert.equal(h.runs.length, 0, "an invalid monitor never starts a process");
161
+ } finally { h.controller.cancelAll(); h.cleanup(); }
162
+ });
163
+
164
+ test("a timeout that fires after the command already exited reports nothing; the exit wins", async () => {
165
+ const h = monitorHarness();
166
+ try {
167
+ const job = h.controller.start({ command: "watch", cwd: "/repo", ownerSessionId: "s1", timeoutSeconds: 1 });
168
+ h.advance(900);
169
+ h.line("last line");
170
+ h.runs[0]!.exit(0);
171
+ await Promise.resolve();
172
+ await Promise.resolve();
173
+ assert.equal(job.status, "exited", "the exit is recorded before its log closes");
174
+ h.advance(100);
175
+ assert.deepEqual(h.notices, [], "no stopped notice for a monitor that ended on its own");
176
+ await h.completion;
177
+ assert.equal(h.settled[0], job);
178
+ h.controller.finish(job);
179
+ assert.deepEqual(h.notices, [{ job, kind: "events", batch: { lines: ["last line"], omitted: 0 } }], "its pending line still arrives");
180
+ } finally { h.controller.cancelAll(); h.cleanup(); }
181
+ });
182
+
183
+ test("stopped notices name the timeout or flood limit and where the full output is", async () => {
184
+ const { monitorStoppedText } = await import("../lib/background-jobs-tools.ts");
185
+ const h = monitorHarness();
186
+ try {
187
+ const job = h.controller.start({ command: "tail -f log", cwd: "/repo", ownerSessionId: "s1", label: "log", timeoutSeconds: 30 });
188
+ h.advance(30_000);
189
+ const timeout = monitorStoppedText(job, "timeout");
190
+ assert.match(timeout, /reached its 30s timeout after 0 events/);
191
+ assert.ok(timeout.includes(`Full output: ${job.outputPath}`));
192
+ const flood = monitorStoppedText(job, "flood");
193
+ assert.ok(flood.includes(`Full output: ${job.outputPath}`));
194
+ } finally { h.controller.cancelAll(); h.cleanup(); }
195
+ });
@@ -8,10 +8,11 @@ import { resolveDefaultSubagentMode } from "../extensions/gentle-agents.ts";
8
8
  // neither an explicit params.mode nor an agent-defined mode was given.
9
9
  //
10
10
  // Background is a runtime default only when the background-subagents policy
11
- // is "on" AND the parent can receive background results. Print mode exits
12
- // before a parent session exists to deliver a background result to, so it
13
- // must keep the configured default even when the policy is on (see the
14
- // `ctx.mode === "print"` guard in gentle-agents.ts `launch`).
11
+ // is "on" AND the parent can receive background results. The single-shot
12
+ // modes (`pi -p` and `pi --mode json`) dispose the runtime once the prompt
13
+ // returns, before a background result can be delivered, so they must keep
14
+ // the configured default even when the policy is on (see the single-shot
15
+ // guard in gentle-agents.ts `launch`).
15
16
  // ---------------------------------------------------------------------------
16
17
 
17
18
  test("policy on + interactive parent -> background", () => {
@@ -25,6 +26,28 @@ test("policy on + interactive parent -> background", () => {
25
26
  );
26
27
  });
27
28
 
29
+ test("policy on + tui parent -> background", () => {
30
+ assert.equal(
31
+ resolveDefaultSubagentMode({
32
+ configuredDefault: AGENT_MODE.TASK,
33
+ policy: "on",
34
+ parentMode: "tui",
35
+ }),
36
+ AGENT_MODE.BACKGROUND,
37
+ );
38
+ });
39
+
40
+ test("policy on + json parent -> configured default (task), never background", () => {
41
+ assert.equal(
42
+ resolveDefaultSubagentMode({
43
+ configuredDefault: AGENT_MODE.TASK,
44
+ policy: "on",
45
+ parentMode: "json",
46
+ }),
47
+ AGENT_MODE.TASK,
48
+ );
49
+ });
50
+
28
51
  test("policy on + rpc parent -> background", () => {
29
52
  assert.equal(
30
53
  resolveDefaultSubagentMode({
@@ -48,7 +71,7 @@ test("policy on + print parent -> configured default (task), never background",
48
71
  });
49
72
 
50
73
  test("policy off -> configured default regardless of parent mode", () => {
51
- for (const parentMode of ["interactive", "rpc", "print", undefined]) {
74
+ for (const parentMode of ["interactive", "tui", "rpc", "print", "json", undefined]) {
52
75
  assert.equal(
53
76
  resolveDefaultSubagentMode({
54
77
  configuredDefault: AGENT_MODE.TASK,
@@ -0,0 +1,85 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import { __testing } from "../extensions/gentle-ai.ts";
6
+
7
+ // gentle-shell#1731 T23 (G1 audit L48): port the gentle-ai Native Checking
8
+ // Contract bounded stop. Partial, blocked, unavailable, or exhausted proof
9
+ // becomes one **Needs your decision** result instead of more verification; the
10
+ // close gate accepts that result as a stop; writer self-review and worker
11
+ // validation stop after a bounded correction; a quick check runs once; an
12
+ // unavailable verifier is reported, never retried or escalated.
13
+ // These are instruction-delivery contracts, not proof of model adherence.
14
+
15
+ const read = (relative: string): string => readFileSync(join(import.meta.dirname, "..", relative), "utf8");
16
+ const verification = read("assets/orchestrator-verification.md");
17
+ const tracking = read("assets/orchestrator-tracking.md");
18
+ const worker = read("assets/agents/gentle-ai-worker.md");
19
+ const verify = read("assets/agents/gentle-ai-verify.md");
20
+ const personas = ["gentleman", "neutral"] as const;
21
+
22
+ function lineStarting(text: string, prefix: string): string {
23
+ const line = text.split("\n").find((entry) => entry.startsWith(prefix));
24
+ assert.ok(line, `missing line starting with: ${prefix}`);
25
+ return line;
26
+ }
27
+
28
+ function containsAll(text: string, clauses: readonly string[], label: string): void {
29
+ for (const clause of clauses) assert.ok(text.includes(clause), `${label} is missing: ${clause}`);
30
+ }
31
+
32
+ function closeStep(persona: (typeof personas)[number]): string {
33
+ return lineStarting(__testing.buildGentlePrompt(persona), "7. **Close.**");
34
+ }
35
+
36
+ test("T23(a): a partial or blocked writer gets the one correction, then Needs your decision, never more verify", () => {
37
+ containsAll(lineStarting(verification, "5. **One correction**"), [
38
+ "A `partial` or `blocked` writer report gets only this correction, never an extra verify run beyond the risk tier's verifier",
39
+ "partial, blocked, unavailable, or exhausted proof left after it becomes that one **Needs your decision**",
40
+ ], "verification correction bound");
41
+ for (const persona of personas) {
42
+ containsAll(closeStep(persona), [
43
+ "Partial, blocked, unavailable, or exhausted proof becomes one **Needs your decision** result naming the open blockers or missing proof, never more verification",
44
+ ], `${persona} harness Close step`);
45
+ }
46
+ });
47
+
48
+ test("T23(b): the close gate accepts one Needs your decision result as a stop, hedged wording still is not", () => {
49
+ for (const persona of personas) {
50
+ containsAll(closeStep(persona), [
51
+ "Never end with a tracked task pending unless you quote the user's explicit stop.",
52
+ "that result is a valid stop, hedged wording is not",
53
+ ], `${persona} harness Close step`);
54
+ }
55
+ containsAll(tracking, [
56
+ "continue it or quote the exact user sentence that orders the stop. One **Needs your decision** result naming the open blockers or missing proof is also a valid stop.",
57
+ "Hedged delivery wording (the user may stop you, review commits one at a time, or pick the work up later) asks for resumable notes and separate commits, not a stop",
58
+ ], "tracking Close gate");
59
+ });
60
+
61
+ test("T23(c): writer self-review and worker validation stop after a bounded correction", () => {
62
+ containsAll(lineStarting(verification, "1. **Self-review**"), [
63
+ "fixes and continues (one correction per failing check, a second only if the same check still fails, then `partial`)",
64
+ ], "self-review item");
65
+ containsAll(worker, [
66
+ "Do not claim completion while required validation is failing.",
67
+ "Make one correction attempt per failing check, and a second only if the same check still fails after a real fix; then stop and return `status: partial` with the failing command and its output, never looping.",
68
+ ], "worker Test discipline");
69
+ });
70
+
71
+ test("T23(d)(e): a quick check runs once and an unavailable verifier is reported, never retried", () => {
72
+ for (const persona of personas) {
73
+ containsAll(closeStep(persona), [
74
+ "An applicable quick check runs once",
75
+ "an unavailable verifier or subagent is reported as unavailable, never retried or escalated into extra ceremony",
76
+ ], `${persona} harness Close step`);
77
+ }
78
+ });
79
+
80
+ test("T23(f): the verify agent allows a second correction only for the same blocker still failing", () => {
81
+ containsAll(lineStarting(verify, "5. **Every item.**"), [
82
+ "one correction batch and one recheck limited to the reported blockers",
83
+ "a second correction only when the recheck shows the same blocker still failing, never for a new finding",
84
+ ], "verify item 5");
85
+ });
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import test from "node:test";
7
7
  import { __testing } from "../extensions/gentle-ai.ts";
8
- import { bindSessionRepositoryPreparation, boundSessionRepositoryAuthorityCurrent, captureBoundSessionRepositoryAuthority, isDevelopmentSurface, prepareBoundSessionRepository, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
8
+ import { allowedEditSurfaces, inheritAllowedEditSurfaces, isBoundedWriter, bindSessionRepositoryPreparation, boundSessionRepositoryAuthorityCurrent, captureBoundSessionRepositoryAuthority, isDevelopmentSurface, prepareBoundSessionRepository, safeBootstrapDirectory, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
9
9
  import type { NativeReviewCli } from "../lib/native-review-cli.ts";
10
10
 
11
11
  test("development admission excludes sensitive, config and bookkeeping surfaces structurally", () => {
@@ -93,3 +93,54 @@ test("explicit review protects canonical HOME aliases, filesystem root and sensi
93
93
  }
94
94
  assert.equal(calls, 0, "no native STATUS against protected directories");
95
95
  });
96
+
97
+ // A Git repository created in HOME, an ancestor of it, or a broad user folder
98
+ // turns every later Git call in the user's tree into a scan of everything
99
+ // below it (a Windows user measured ~47s startups). Bootstrap refuses them.
100
+ test("native bootstrap refuses HOME ancestors and broad user folders but keeps project folders", t => {
101
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "bootstrap-broad-")));
102
+ t.after(() => rmSync(root, { recursive: true, force: true }));
103
+ const home = join(root, "home");
104
+ const broad = ["Documents", "Desktop", "Downloads", "OneDrive", "OneDrive - Contoso", join("OneDrive", "Documents")];
105
+ for (const folder of [...broad, join("Documents", "project"), "project"]) mkdirSync(join(home, folder), { recursive: true });
106
+ for (const target of [root, home, ...broad.map(folder => join(home, folder))]) assert.equal(safeBootstrapDirectory(target, { home }), undefined, target);
107
+ for (const target of [join(home, "project"), join(home, "Documents", "project")]) assert.equal(safeBootstrapDirectory(target, { home }), target, target);
108
+ });
109
+
110
+ test("native bootstrap compares HOME case-insensitively on Windows only", t => {
111
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "bootstrap-case-")));
112
+ t.after(() => rmSync(root, { recursive: true, force: true }));
113
+ const home = join(root, "home");
114
+ mkdirSync(join(home, "project"), { recursive: true });
115
+ const differentlyCasedHome = home.toUpperCase();
116
+ assert.equal(safeBootstrapDirectory(home, { home: differentlyCasedHome, platform: "win32" }), undefined);
117
+ assert.equal(safeBootstrapDirectory(home, { home: differentlyCasedHome, platform: "linux" }), home);
118
+ assert.equal(safeBootstrapDirectory(join(home, "project"), { home: differentlyCasedHome, platform: "win32" }), join(home, "project"));
119
+ });
120
+
121
+ // gentle-shell#1713: a writer continuation without its own section was
122
+ // rejected, and every retry shortened the follow-up. A continuation resumes
123
+ // the same delegated task, so it inherits the surfaces that launch admitted.
124
+ test("writer continuations inherit the original surfaces only when they carry none", () => {
125
+ const original = "Implement it.\n\n## Allowed edit surfaces\n- src/model.ts\n- `docs/with space.md`\n\n## Return\nReport";
126
+ const inherited = inheritAllowedEditSurfaces("gentle-ai-worker", "Continue with the remaining specs.", undefined, original);
127
+ assert.deepEqual(allowedEditSurfaces(inherited), ["docs/with space.md", "src/model.ts"]);
128
+ assert.ok(inherited.startsWith("Continue with the remaining specs."));
129
+ const own = "Continue.\n## Allowed edit surfaces\nsrc/other.ts";
130
+ assert.equal(inheritAllowedEditSurfaces("gentle-ai-worker", own, undefined, original), own, "a follow-up section is validated as written, never merged");
131
+ const viaContext = "## Allowed edit surfaces\nsrc/other.ts";
132
+ assert.equal(inheritAllowedEditSurfaces("worker", "Continue.", viaContext, original), "Continue.");
133
+ assert.equal(inheritAllowedEditSurfaces("gentle-ai-explore", "Continue.", undefined, original), "Continue.", "non-writers are untouched");
134
+ assert.equal(inheritAllowedEditSurfaces("jd-fix-agent", "Continue.", undefined, original), "Continue.", "Judgment Day fix batches keep their exact protocol");
135
+ assert.equal(inheritAllowedEditSurfaces("gentle-ai-worker", "Continue.", undefined, "No surfaces here."), "Continue.", "nothing to inherit stays rejectable");
136
+ // Review R3-002: an entry admitted only when quoted must round-trip quoted.
137
+ const quotedOnly = "## Allowed edit surfaces\n- `-`\n- src/model.ts";
138
+ assert.deepEqual(allowedEditSurfaces(quotedOnly), ["-", "src/model.ts"]);
139
+ assert.deepEqual(allowedEditSurfaces(inheritAllowedEditSurfaces("worker", "Continue.", undefined, quotedOnly)), ["-", "src/model.ts"]);
140
+ });
141
+
142
+ // gentle-shell#1731: only agents behind the surfaces guard claim surfaces at runtime.
143
+ test("bounded writers are exactly the agents behind the Allowed edit surfaces guard", () => {
144
+ for (const name of ["gentle-ai-worker", "worker", "jd-fix-agent"]) assert.equal(isBoundedWriter(name), true, name);
145
+ for (const name of ["explore", "gentle-ai-explore", "gentle-ai-verify", "reviewer", ""]) assert.equal(isBoundedWriter(name), false, name);
146
+ });