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
@@ -1,4 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
+ import { parseNpmPackResult } from "../scripts/npm-pack-result.mjs";
2
3
  import { execFileSync } from "node:child_process";
3
4
  import { createHash } from "node:crypto";
4
5
  import {
@@ -60,7 +61,7 @@ const V014_MANAGED_ASSETS = join(
60
61
  // gentle-ai-explore.md as their exemplar (packaged, absent from the v0.13
61
62
  // manifest) after review-refuter.md was retired together with every
62
63
  // Pi-authored adversarial review verdict.
63
- const MANAGED_EXEMPLAR_TOOLS = ["read", "grep", "find", "codegraph"];
64
+ const MANAGED_EXEMPLAR_TOOLS = ["read", "grep", "find", "codegraph", "web_enable", "web_search", "source_check", "fetch_content", "get_search_content"];
64
65
  const RETIRED_ADVERSARIAL_AGENTS = ["review-refuter.md", "review-validator.md"];
65
66
 
66
67
  interface ManagedAssetsManifest {
@@ -132,13 +133,13 @@ test("public docs and metadata advertise ODD and review without retired phase wo
132
133
 
133
134
  test("technical reference declares the tested Pi minimum required for agent_settled", () => {
134
135
  const manifest = readPackageJson();
135
- assert.equal(manifest.peerDependencies?.["@earendil-works/pi-coding-agent"], ">=0.99.1");
136
- assert.equal(manifest.devDependencies?.["@earendil-works/pi-coding-agent"], ">=1.0.0");
136
+ assert.equal(manifest.peerDependencies?.["@earendil-works/pi-coding-agent"], ">=1.2.0");
137
+ assert.equal(manifest.devDependencies?.["@earendil-works/pi-coding-agent"], ">=1.2.0");
137
138
  assert.equal(manifest.peerDependenciesMeta?.["@earendil-works/pi-coding-agent"]?.optional, true);
138
139
  assert.equal(manifest.engines?.node, ">=22.19.0");
139
140
  for (const path of ["docs/readme-reference.md", "docs/gentle-shell.md"]) {
140
141
  const source = readFileSync(join(PACKAGE_ROOT, path), "utf8");
141
- assert.match(source, /Pi 0\.99\.1 or newer/, path);
142
+ assert.match(source, /Pi 1\.2\.0 or newer/, path);
142
143
  assert.match(source, /open `>=1\.0\.0` development range/, path);
143
144
  assert.doesNotMatch(source, /development tests pin Pi/, path);
144
145
  // Docs name exactly the audited Vim editor releases, never a future one.
@@ -161,7 +162,7 @@ test("packed runtime uses optional Pi host peers with one open development range
161
162
  // one exact release shared by every Pi host package.
162
163
  const installed = new Set<string>();
163
164
  for (const name of ["@earendil-works/pi-coding-agent", "@earendil-works/pi-ai", "@earendil-works/pi-tui"]) {
164
- assert.equal(manifest.devDependencies?.[name], ">=1.0.0", name);
165
+ assert.equal(manifest.devDependencies?.[name], ">=1.2.0", name);
165
166
  const metadata = JSON.parse(readFileSync(join(PACKAGE_ROOT, "node_modules", name, "package.json"), "utf8")) as { name: string; version: string };
166
167
  assert.equal(metadata.name, name);
167
168
  installed.add(metadata.version);
@@ -253,6 +254,25 @@ test("package verification names the native review runtime boundary and packaged
253
254
  );
254
255
  });
255
256
 
257
+ test("double-click installers are attached to the release only after the verified publication", () => {
258
+ const workflow = readFileSync(join(PACKAGE_ROOT, ".github", "workflows", "publish.yml"), "utf8");
259
+ const job = workflow.match(/^ {2}installers:\n([\s\S]*?)(?=^ {2}[A-Za-z0-9_-]+:\n|(?![\s\S]))/m)?.[1];
260
+ assert.ok(job, "publish.yml has an installers job");
261
+ assert.match(job, /^ {4}needs: publish$/m, "installers wait for the verified npm publication");
262
+ assert.match(job, /^ {4}if: github\.repository == 'Gentleman-Programming\/gentle-shell'$/m);
263
+ assert.match(job, /^ {6}contents: write$/m, "only this job may write the release");
264
+ assert.match(job, /ref: \$\{\{ github\.sha \}\}/, "the installers come from the verified release commit");
265
+ assert.match(job, /persist-credentials: false/);
266
+ assert.match(job, /node scripts\/build-installer-bundles\.mjs --out "\$\{RUNNER_TEMP\}\/installers"/);
267
+ assert.match(job, /RELEASE_TAG: \$\{\{ needs\.publish\.outputs\.tag \}\}/, "the tag the publish job verified, not the raw input");
268
+ assert.match(job, /package-manager-cache: false/);
269
+ assert.match(workflow, /^ {4}outputs:\n {6}tag: \$\{\{ steps\.release\.outputs\.tag \}\}$/m, "publish exports its verified tag");
270
+ assert.match(job, /gh release upload "\$\{RELEASE_TAG\}" "\$\{RUNNER_TEMP\}"\/installers\/\* --repo "\$\{GITHUB_REPOSITORY\}" --clobber/);
271
+ const publish = workflow.match(/^ {2}publish:\n([\s\S]*?)(?=^ {2}installers:\n)/m)?.[1];
272
+ assert.ok(publish);
273
+ assert.doesNotMatch(publish, /contents: write/, "the npm publication job keeps read-only contents");
274
+ });
275
+
256
276
  test("npm publication is bound to the exact package tag and triggering commit", () => {
257
277
  const workflow = readFileSync(join(PACKAGE_ROOT, ".github", "workflows", "publish.yml"), "utf8");
258
278
  const packageJson = readPackageJson();
@@ -411,20 +431,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
411
431
  );
412
432
  });
413
433
 
414
- test("package verification binds the published Gentle AI v4.0.0 runtime pin", () => {
434
+ test("package verification binds the published Gentle AI v4.1.1 runtime pin", () => {
415
435
  const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
416
436
  const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
417
437
  const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
418
438
 
419
- assert.match(installer, /INSTALLER_VERSION = "4\.0\.0"/);
439
+ assert.match(installer, /INSTALLER_VERSION = "4\.1\.1"/);
420
440
  assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
421
- assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:pZ\/XZ2Pk3U9lgXigOTY62zlxxFOHnc9CjQhLgaV\/Hfc="/);
441
+ assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:\+WmZ2IvIOiJ8qtHa7wsjnYKNasem\+PByeOZ6HIjoAL8="/);
422
442
  assert.match(installer, /GOTOOLCHAIN: "local"/);
423
443
  assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
424
444
  assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
425
445
  assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
426
446
  assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
427
- assert.match(verifier, /v4\.0\.0/);
447
+ assert.match(verifier, /v4\.1\.1/);
428
448
  });
429
449
 
430
450
 
@@ -550,7 +570,7 @@ function readMarkdownSection(source: string, heading: string): string {
550
570
 
551
571
  function assertWorkerFallbackRouting(section: string, sectionName: string): void {
552
572
  const boundedWriterPolicy = section.match(
553
- /For bounded multi-file writes,[\s\S]*?(?=\n\n|\n\s*\d+\.|$)/,
573
+ /For a large task's bounded writes,[\s\S]*?(?=\n\n|\n\s*\d+\.|$)/,
554
574
  )?.[0];
555
575
  assert.ok(boundedWriterPolicy, `${sectionName} must define bounded writer routing`);
556
576
 
@@ -847,6 +867,42 @@ test("selective review migration adopts only untouched legacy copies and preserv
847
867
  }
848
868
  });
849
869
 
870
+ test("native pulse audio ships owned TypeScript sources without new dependencies or addons", () => {
871
+ const manifest = readPackageJson();
872
+ const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
873
+ const sources = [
874
+ "lib/notification-pulse-protocol.ts", "lib/notification-pulse-client.ts",
875
+ "lib/notification-pulse-stream.ts", "lib/notification-audio-native.ts",
876
+ "lib/notification-pulse-worker.ts",
877
+ ];
878
+ for (const source of sources) {
879
+ assert.ok(existsSync(join(PACKAGE_ROOT, source)), `${source} must exist`);
880
+ assert.ok(verifier.includes(`"${source}"`), `${source} must be a required package resource`);
881
+ }
882
+ assert.ok(!manifest.files?.includes("native/"), "no native addon directory may ship");
883
+ for (const name of Object.keys(manifest.dependencies ?? {})) assert.doesNotMatch(name, /pulse|audio|sound|native/i, name);
884
+ for (const name of Object.keys(manifest.scripts ?? {})) assert.doesNotMatch(name, /pulse|native:build|audio:install/i, name);
885
+ for (const path of ["lib/notification-pulse-worker.ts", "lib/notification-audio-native.ts"]) {
886
+ const source = readFileSync(join(PACKAGE_ROOT, path), "utf8");
887
+ assert.doesNotMatch(source, /@earendil-works|\.node["']|addon/i, path);
888
+ assert.doesNotMatch(source, /postinstall|installer|download/i, path);
889
+ }
890
+ });
891
+
892
+ test("native windows audio ships an owned encoded-command adapter without scripts or dependencies", () => {
893
+ const manifest = readPackageJson();
894
+ const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
895
+ const path = "lib/notification-audio-windows.ts";
896
+ assert.ok(existsSync(join(PACKAGE_ROOT, path)), `${path} must exist`);
897
+ assert.ok(verifier.includes(`"${path}"`), `${path} must be a required package resource`);
898
+ for (const dependency of Object.keys(manifest.dependencies ?? {})) assert.doesNotMatch(dependency, /audio|sound|windows|powershell|native/i, dependency);
899
+ for (const script of Object.keys(manifest.scripts ?? {})) assert.doesNotMatch(script, /windows:build|powershell|audio:install/i, script);
900
+ const source = readFileSync(join(PACKAGE_ROOT, path), "utf8");
901
+ assert.match(source, /-EncodedCommand/, "the adapter must drive the trusted built-in PowerShell host with an encoded command");
902
+ assert.doesNotMatch(source, /\.ps1|ExecutionPolicy|\.node["']|addon/i, path);
903
+ assert.doesNotMatch(source, /postinstall|installer|download/i, path);
904
+ });
905
+
850
906
  test("packed tarball excludes retired workflow paths while source retains legacy migration proof", () => {
851
907
  const fixture = "tests/fixtures/legacy/sdd-research-v2.5.0.md";
852
908
  assert.ok(existsSync(join(PACKAGE_ROOT, fixture)), "the historical source fixture must remain available to migration tests");
@@ -857,9 +913,22 @@ test("packed tarball excludes retired workflow paths while source retains legacy
857
913
  encoding: "utf8",
858
914
  maxBuffer: 8 * 1024 * 1024,
859
915
  });
860
- const [packed] = JSON.parse(output) as [{ files: { path: string }[] }];
916
+ const [packed] = parseNpmPackResult(output);
861
917
  assert.ok(packed?.files?.length, "npm pack must return a nonempty tar manifest");
862
918
  assert.ok(packed.files.some(file => file.path === "tests/package-manifest.test.ts"), "other tests remain packed");
919
+ for (const path of [
920
+ "extensions/gentle-notifications.ts",
921
+ "lib/notification-audio.ts", "lib/notification-customize.ts", "lib/notification-events.ts", "lib/notification-policy.ts",
922
+ "lib/notification-scheduler.ts", "lib/notification-service.ts", "lib/notification-ui.ts",
923
+ "assets/sounds/success.wav", "assets/sounds/error.wav", "assets/sounds/attention.wav",
924
+ "assets/sounds/LICENSE.md", "docs/sound-notifications.md", "docs/sound-notifications-proposal.md",
925
+ "lib/notification-pulse-protocol.ts", "lib/notification-pulse-client.ts", "lib/notification-pulse-stream.ts",
926
+ "lib/notification-audio-native.ts", "lib/notification-pulse-worker.ts",
927
+ "lib/notification-audio-windows.ts",
928
+ "scripts/npm-pack-result.mjs",
929
+ ]) {
930
+ assert.ok(packed.files.some(file => file.path === path), `${path} must be packed`);
931
+ }
863
932
  assert.deepEqual(packed.files.filter(file => /sdd|openspec/i.test(file.path)).map(file => file.path), []);
864
933
  } finally {
865
934
  rmSync(destination, { recursive: true, force: true });
@@ -1441,7 +1510,7 @@ test("gentle-ai-worker packages the exact scoped writer contract", () => {
1441
1510
  const testDiscipline = readMarkdownSection(source, "Test discipline");
1442
1511
  assert.match(testDiscipline, /Apply the ODD test-first policy by default for behavior changes with applicable runnable deterministic tests and a clear expected outcome/);
1443
1512
  assert.match(testDiscipline, /Test presence alone does not establish applicability; no TUI toggle or per-task chat choice is needed/);
1444
- assert.match(testDiscipline, /RED[\s\S]*GREEN[\s\S]*TRIANGULATE[\s\S]*REFACTOR/);
1513
+ assert.match(testDiscipline, /RED[\s\S]*GREEN[\s\S]*PRESERVE[\s\S]*REFACTOR/);
1445
1514
  assert.match(testDiscipline, /no meaningful RED[\s\S]*proportionate ordinary functional or structural verification/);
1446
1515
  assert.match(testDiscipline, /Never claim RED\/GREEN evidence that was not observed/);
1447
1516
  assert.match(
@@ -1604,7 +1673,7 @@ test("global model routing uses PI_CODING_AGENT_DIR for package-installed agents
1604
1673
  test("normal and forced installation copy generic agents with complete role contracts", () => {
1605
1674
  const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
1606
1675
  const expectedTools = {
1607
- "gentle-ai-explore": ["read", "grep", "find", "codegraph"],
1676
+ "gentle-ai-explore": ["read", "grep", "find", "codegraph", "web_enable", "web_search", "source_check", "fetch_content", "get_search_content"],
1608
1677
  "gentle-ai-verify": ["read", "grep", "find", "bash"],
1609
1678
  } as const;
1610
1679
 
@@ -1636,7 +1705,7 @@ test("normal and forced installation copy generic agents with complete role cont
1636
1705
  assert.match(source, /compressed (?:handoff|evidence handoff)/);
1637
1706
  assert.match(source, /Do not use review lenses\. RDD review remains independent and parent-owned\./);
1638
1707
  if (name === "gentle-ai-verify") {
1639
- assert.match(source, /exact test, build, or lint commands explicitly authorized by the parent/);
1708
+ assert.match(source, /exact test, build, lint, or spec example commands explicitly authorized by the parent/);
1640
1709
  assert.match(source, /only outputs the parent explicitly identified as expected/);
1641
1710
  assert.match(source, /unexpected mutation as a blocker/);
1642
1711
  assert.match(source, /do not clean it up or fix it/);
@@ -1652,16 +1721,30 @@ test("normal and forced installation copy generic agents with complete role cont
1652
1721
  }
1653
1722
  });
1654
1723
 
1655
- test("bounded implementation routing uses the same explicit fallback in both policy sections", () => {
1724
+ test("bounded implementation routing resolves the explicit canonical fallback reference", () => {
1656
1725
  const routing = readFileSync(
1657
1726
  join(PACKAGE_ROOT, "assets", "orchestrator-delegation.md"),
1658
1727
  "utf8",
1659
1728
  );
1660
- const simpleDelegation = readMarkdownSection(routing, "2. Simple Delegation");
1729
+ const reference = "For bounded writes, follow the canonical Writer rule under Mandatory Delegation Triggers.";
1730
+ const resolveSimpleDelegation = (source: string): string => {
1731
+ const simpleDelegation = readMarkdownSection(source, "2. Simple Delegation");
1732
+ assert.ok(simpleDelegation.split("\n").includes(reference), "Simple Delegation must name the exact canonical Writer rule");
1733
+ const canonical = readMarkdownSection(source, "Mandatory Delegation Triggers");
1734
+ assertWorkerFallbackRouting(canonical, "resolved Simple Delegation");
1735
+ return canonical;
1736
+ };
1661
1737
  const mandatoryDelegation = readMarkdownSection(routing, "Mandatory Delegation Triggers");
1662
1738
 
1663
- assertWorkerFallbackRouting(simpleDelegation, "Simple Delegation");
1739
+ assertWorkerFallbackRouting(resolveSimpleDelegation(routing), "Simple Delegation");
1664
1740
  assertWorkerFallbackRouting(mandatoryDelegation, "Mandatory Delegation Triggers");
1741
+ assert.throws(() => resolveSimpleDelegation(routing.replace(reference, "")), /must name the exact canonical Writer rule/);
1742
+ assert.throws(() => resolveSimpleDelegation(routing.replace(reference, reference.replace("Mandatory Delegation Triggers", "Other Rule"))),
1743
+ /must name the exact canonical Writer rule/);
1744
+ assert.throws(() => resolveSimpleDelegation(routing.replace("#### Mandatory Delegation Triggers", "#### Missing Canonical Rule")),
1745
+ /exactly one Mandatory Delegation Triggers section/);
1746
+ assert.throws(() => resolveSimpleDelegation(routing.replace("user-configured `worker`", "unspecified worker")),
1747
+ /must prefer the package-owned worker before a user-configured worker/);
1665
1748
  assert.doesNotMatch(
1666
1749
  routing,
1667
1750
  /non-normative compatibility quotation|former wording is retained|no-runtime inline exception|superseded by the stop requirement/,
@@ -1680,8 +1763,9 @@ test("orchestrator routes generic roles without static RDD lens routing", () =>
1680
1763
  assert.match(routing, /`gentle-ai-explore`/);
1681
1764
  assert.match(routing, /`gentle-ai-worker`/);
1682
1765
  assert.match(routing, /`gentle-ai-verify`/);
1683
- assert.match(routing, /read-only check within the evidence budget/);
1684
- assert.match(routing, /(?:verification that |verification commands →).*executes? or delegates?|executing\/delegating verification commands/);
1766
+ assert.match(routing, /focused test and (?:the )?suite/);
1767
+ // The Verification rule line itself must route high risk to the verifier.
1768
+ assert.match(routing, /^\d\. \*\*Verification rule\*\*[^\n]*high[- ]risk[^\n]*`gentle-ai-verify`/m);
1685
1769
  assert.match(routing, /missing(?: or |\/)unusable[\s\S]*native `Agent`[\s\S]*(?:the )?same read-only/);
1686
1770
  assert.match(routing, /report (?:the )?fallback/);
1687
1771
  assert.doesNotMatch(routing, /review lenses? (?:inside|only inside)|review lens routing/i);
@@ -1705,9 +1789,9 @@ test("pi-pretty wrapper uses cached ESM loading for compiled and pnpm symlink in
1705
1789
  assert.match(wrapper, /quietToolsEnabled/);
1706
1790
  });
1707
1791
 
1708
- test("Gentle Shell v4.0.0 package manifest declares the release version", () => {
1792
+ test("Gentle Shell v4.2.0 package manifest declares the release version", () => {
1709
1793
  const packageJson = readPackageJson();
1710
- assert.equal(packageJson.version, "4.0.0", "the release manifest must be explicitly pinned to v4.0.0");
1794
+ assert.equal(packageJson.version, "4.2.0", "the release manifest must be explicitly pinned to v4.2.0");
1711
1795
  assert.equal(packageJson.scripts?.test, "node scripts/run-test-suite.mjs");
1712
1796
  assert.ok(packageJson.files?.includes("assets/"));
1713
1797
  assert.ok(packageJson.files?.includes("contracts/"));
@@ -1,9 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { join, resolve, sep } from "node:path";
5
5
  import test from "node:test";
6
6
  import { resolveInstalledJitiStaticEntry } from "../scripts/test-packed-runner.mjs";
7
+ import * as packedRunner from "../scripts/test-packed-runner.mjs";
7
8
 
8
9
  function withAliasedConsumer(run: (consumer: string, sdkManifest: string, jitiRoot: string) => void): void {
9
10
  const disposable = mkdtempSync(join(tmpdir(), "packed-owned-path-"));
@@ -24,6 +25,39 @@ function withAliasedConsumer(run: (consumer: string, sdkManifest: string, jitiRo
24
25
  }
25
26
  }
26
27
 
28
+ test("hooked consumer approves only the canonical local tarball file spec", () => {
29
+ const disposable = mkdtempSync(join(tmpdir(), "packed-approval-"));
30
+ try {
31
+ const consumer = join(disposable, "install");
32
+ const pack = join(disposable, "pack");
33
+ mkdirSync(consumer);
34
+ mkdirSync(pack);
35
+ const tarball = join(pack, "gentle-pi-4.0.0.tgz");
36
+ writeFileSync(tarball, "owned tarball fixture");
37
+ const manifest = packedRunner.createHookedPackedConsumerManifest(consumer, tarball);
38
+ assert.deepEqual(manifest, {
39
+ name: "gentle-pi-packed-runner-test", private: true,
40
+ allowScripts: {
41
+ "file:../pack/gentle-pi-4.0.0.tgz": true,
42
+ [`file:${realpathSync.native(tarball).split(sep).join("/")}`]: true,
43
+ },
44
+ });
45
+ assert.equal(manifest.allowScripts["gentle-pi"], undefined);
46
+ assert.equal(manifest.allowScripts["*"], undefined);
47
+ for (const spec of Object.keys(manifest.allowScripts)) {
48
+ const filePath = spec.slice(5);
49
+ assert.equal(realpathSync.native(resolve(consumer, filePath)), realpathSync.native(tarball));
50
+ }
51
+ symlinkSync(consumer, join(disposable, "consumer-alias"), "dir");
52
+ assert.deepEqual(packedRunner.createHookedPackedConsumerManifest(join(disposable, "consumer-alias"), tarball), manifest);
53
+ symlinkSync("gentle-pi-4.0.0.tgz", join(pack, "alias.tgz"));
54
+ assert.throws(() => packedRunner.createHookedPackedConsumerManifest(consumer, join(pack, "alias.tgz")), /symbolic link/);
55
+ assert.throws(() => packedRunner.createHookedPackedConsumerManifest(consumer, pack), /regular file/);
56
+ } finally {
57
+ rmSync(disposable, { recursive: true, force: true });
58
+ }
59
+ });
60
+
27
61
  const checks = { sdkManifest: "sdk-manifest", sdkVersion: "sdk-version", jitiManifest: "jiti-manifest-owned", jitiStaticExport: "jiti-static-export", jitiEntry: "jiti-entry-owned", jitiVersion: "jiti-version" };
28
62
 
29
63
  test("accepts a canonical Jiti manifest beneath an aliased ancestor", () => {
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ // gentle-shell#1731 T29 (L66): in the targeted bench (B, pi --mode json,
7
+ // background policy off) the parallelism reason fired for two independent
8
+ // features, but the writers ran one after the other in task mode: no
9
+ // parallelism, only subagent overhead (x5 took 10.9 min vs 4.5 inline). The
10
+ // lazy writer module already voided serial launches, but it loads only after
11
+ // the core rule fires, so the condition must live in the core Writer rule.
12
+ const core = readFileSync(join(import.meta.dirname, "..", "assets/orchestrator.md"), "utf8");
13
+
14
+ test("T29: the core parallelism reason requires launching the units together in background", () => {
15
+ const rule = core.split("\n").find((line) => line.includes("5. **Writer rule**"));
16
+ assert.ok(rule, "core Writer rule is missing");
17
+ assert.ok(
18
+ rule.includes("launched together in background, else inline"),
19
+ "parallelism must require background launches in the same turn",
20
+ );
21
+ });
@@ -0,0 +1,210 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
4
+ import { TASK_STATUS, TaskStore } from "../lib/agents-protocol.ts";
5
+ import { AgentRunner, type TaskRequest } from "../lib/agents-runner.ts";
6
+ import { fakeChild, type FakeChild } from "./agents-fake-child.ts";
7
+
8
+ // gentle-shell#1731 T4 (S2, AC6): the runner admits a writer only while no
9
+ // queued or running writer in the same worktree root claims an overlapping
10
+ // `## Allowed edit surfaces` entry. Admission and registration happen in the
11
+ // same synchronous run() call, and every terminal path releases the claim.
12
+
13
+ const worker: AgentDefinition = { name: "gentle-ai-worker", description: "writes", filePath: "/a/worker.md", scope: "global", instructions: "You write.", model: undefined, thinking: undefined, mode: undefined, tools: ["read", "edit", "write"] };
14
+ const explorer: AgentDefinition = { ...worker, name: "explore", description: "maps", tools: ["read"] };
15
+
16
+ function writer(surfaces: string[], overrides: Partial<TaskRequest> = {}): TaskRequest {
17
+ return { agent: worker, prompt: `Write.\n\n## Allowed edit surfaces\n${surfaces.join("\n")}\n`, label: undefined, context: undefined, mode: AGENT_MODE.BACKGROUND, cwd: "/repo", parentSessionId: "s1", model: undefined, thinking: undefined, sessionDir: "/sessions", resumeSessionPath: undefined, env: {}, writerSurfaces: surfaces, ...overrides };
18
+ }
19
+
20
+ function harness(maxConcurrency = 5) {
21
+ const children: FakeChild[] = [];
22
+ const timers: Array<{ fn: () => void; cancelled: boolean }> = [];
23
+ const store = new TaskStore();
24
+ const runner = new AgentRunner(store, { maxConcurrency, stallTimeoutMs: 10_000 }, {
25
+ spawn: () => {
26
+ const fake = fakeChild();
27
+ children.push(fake);
28
+ return fake.child;
29
+ },
30
+ now: () => Date.now(),
31
+ schedule: (fn) => {
32
+ const timer = { fn, cancelled: false };
33
+ timers.push(timer);
34
+ return () => { timer.cancelled = true; };
35
+ },
36
+ pi: { command: "pi", args: [] },
37
+ }, { askUser: async () => ({ cancelled: true }) });
38
+ const fireTimers = () => { for (const timer of timers.splice(0)) if (!timer.cancelled) timer.fn(); };
39
+ return { store, runner, children, fireTimers };
40
+ }
41
+
42
+ const tick = () => new Promise((resolve) => setImmediate(resolve));
43
+
44
+ async function complete(h: ReturnType<typeof harness>, index: number, id: string): Promise<void> {
45
+ h.children[index].emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }] });
46
+ h.children[index].emit({ type: "agent_settled" });
47
+ await h.runner.waitFor(id);
48
+ }
49
+
50
+ test("writers with disjoint surfaces run concurrently in one worktree", async () => {
51
+ const h = harness();
52
+ const a = h.runner.run(writer(["lib/a.ts", "tests/a.test.ts"]));
53
+ const b = h.runner.run(writer(["lib/b.ts", "tests/b.test.ts"]));
54
+ await tick();
55
+ assert.equal(h.children.length, 2);
56
+ assert.equal(h.store.get(a.id)?.status, TASK_STATUS.RUNNING);
57
+ assert.equal(h.store.get(b.id)?.status, TASK_STATUS.RUNNING);
58
+ h.runner.cancelAll();
59
+ });
60
+
61
+ test("an overlapping writer is rejected before any task, queue entry or spawn exists", async () => {
62
+ const h = harness();
63
+ const a = h.runner.run(writer(["lib/a.ts", "tests/a.test.ts"]));
64
+ await tick();
65
+ assert.throws(() => h.runner.run(writer(["lib/*.ts"])), (error: Error) => {
66
+ assert.match(error.message, new RegExp(`task ${a.id}`));
67
+ assert.match(error.message, /`lib\/\*\.ts` overlaps `lib\/a\.ts`/);
68
+ assert.match(error.message, /isolated worktree/);
69
+ return true;
70
+ });
71
+ assert.equal(h.store.list("s1").length, 1, "the rejected writer leaves no task record");
72
+ await tick();
73
+ assert.equal(h.children.length, 1, "the rejected writer never spawns");
74
+ h.runner.cancelAll();
75
+ });
76
+
77
+ test("a queued writer already holds its surfaces", async () => {
78
+ const h = harness(1);
79
+ h.runner.run(writer(["lib/a.ts"]));
80
+ const queued = h.runner.run(writer(["lib/b.ts"]));
81
+ await tick();
82
+ assert.equal(h.store.get(queued.id)?.status, TASK_STATUS.QUEUED);
83
+ assert.throws(() => h.runner.run(writer(["lib/b.ts"])), new RegExp(`task ${queued.id}`));
84
+ h.runner.cancelAll();
85
+ });
86
+
87
+ test("completion releases the claim", async () => {
88
+ const h = harness();
89
+ const a = h.runner.run(writer(["lib/a.ts"]));
90
+ await tick();
91
+ await complete(h, 0, a.id);
92
+ assert.equal(h.store.get(a.id)?.status, TASK_STATUS.COMPLETED);
93
+ const next = h.runner.run(writer(["lib/a.ts"]));
94
+ assert.ok(next.id);
95
+ h.runner.cancelAll();
96
+ });
97
+
98
+ test("failure releases the claim", async () => {
99
+ const h = harness();
100
+ const a = h.runner.run(writer(["lib/a.ts"]));
101
+ await tick();
102
+ h.children[0].exit(1);
103
+ await h.runner.waitFor(a.id);
104
+ assert.equal(h.store.get(a.id)?.status, TASK_STATUS.FAILED);
105
+ assert.doesNotThrow(() => h.runner.run(writer(["lib/a.ts"])));
106
+ h.runner.cancelAll();
107
+ });
108
+
109
+ test("a spawn failure releases the claim", async () => {
110
+ const h = harness();
111
+ const a = h.runner.run(writer(["lib/a.ts"], { beforeSpawn: () => { throw new Error("session changed"); } }));
112
+ await h.runner.waitFor(a.id);
113
+ assert.equal(h.store.get(a.id)?.status, TASK_STATUS.FAILED);
114
+ assert.doesNotThrow(() => h.runner.run(writer(["lib/a.ts"])));
115
+ h.runner.cancelAll();
116
+ });
117
+
118
+ test("cancelling a running or queued writer releases its claim", async () => {
119
+ const h = harness(1);
120
+ const running = h.runner.run(writer(["lib/a.ts"]));
121
+ const queued = h.runner.run(writer(["lib/b.ts"]));
122
+ await tick();
123
+ assert.equal(h.runner.cancel(queued.id), true);
124
+ assert.equal(h.store.get(queued.id)?.status, TASK_STATUS.CANCELLED);
125
+ assert.doesNotThrow(() => h.runner.run(writer(["lib/b.ts"])));
126
+ assert.equal(h.runner.cancel(running.id), true);
127
+ await h.runner.waitFor(running.id);
128
+ assert.equal(h.store.get(running.id)?.status, TASK_STATUS.CANCELLED);
129
+ assert.doesNotThrow(() => h.runner.run(writer(["lib/a.ts"])));
130
+ h.runner.cancelAll();
131
+ });
132
+
133
+ test("a stall timeout releases the claim", async () => {
134
+ const h = harness();
135
+ const a = h.runner.run(writer(["lib/a.ts"]));
136
+ await tick();
137
+ h.fireTimers();
138
+ await h.runner.waitFor(a.id);
139
+ assert.equal(h.store.get(a.id)?.status, TASK_STATUS.TIMED_OUT);
140
+ assert.doesNotThrow(() => h.runner.run(writer(["lib/a.ts"])));
141
+ h.runner.cancelAll();
142
+ });
143
+
144
+ // Verify m1: a quarantined writer may still be alive, so its claim lives as
145
+ // long as its reserved concurrency slot and is released only on proven exit.
146
+ test("a quarantined writer keeps its claim until its exit is confirmed", async () => {
147
+ const store = new TaskStore();
148
+ const timers: Array<{ fn: () => void; ms: number; cancelled: boolean }> = [];
149
+ let now = 0;
150
+ let groupGone = false;
151
+ const child = fakeChild({ exitOnKill: false, pid: 71 });
152
+ const runner = new AgentRunner(store, { maxConcurrency: 5, stallTimeoutMs: 10_000 }, {
153
+ spawn: () => child.child,
154
+ now: () => now,
155
+ schedule: (fn, ms) => {
156
+ const timer = { fn, ms, cancelled: false };
157
+ timers.push(timer);
158
+ return () => { timer.cancelled = true; };
159
+ },
160
+ pi: { command: "pi", args: [] },
161
+ process: { platform: "linux", kill: (_pid, signal) => {
162
+ if (signal === 0) throw Object.assign(new Error("group probe"), { code: groupGone ? "ESRCH" : "EPERM" });
163
+ } },
164
+ }, { askUser: async () => ({ cancelled: true }) });
165
+ const first = runner.run(writer(["lib/a.ts"]));
166
+ await tick();
167
+ runner.cancel(first.id);
168
+ timers.find((timer) => timer.ms === 250)!.fn();
169
+ now = 2_000;
170
+ timers.filter((timer) => timer.ms === 25).at(-1)!.fn();
171
+ await tick();
172
+ assert.match(store.get(first.id)?.error ?? "", /capacity quarantined/);
173
+ assert.throws(() => runner.run(writer(["lib/a.ts"])), new RegExp(`task ${first.id}`), "the quarantined writer still holds its surfaces");
174
+ groupGone = true;
175
+ child.exit(0);
176
+ await tick();
177
+ assert.doesNotThrow(() => runner.run(writer(["lib/a.ts"])), "proven exit releases the claim");
178
+ runner.cancelAll();
179
+ });
180
+
181
+ // Second verify A1: the claim key is the canonical worktree root supplied by
182
+ // the parent, not the spawn cwd, so a subdirectory cwd and the root conflict.
183
+ test("writers keyed to the same canonical root conflict whatever their cwd", async () => {
184
+ const h = harness();
185
+ const first = h.runner.run(writer(["lib/a.ts"], { cwd: "/repo/sub", writerRoot: "/repo" }));
186
+ assert.throws(() => h.runner.run(writer(["lib/a.ts"], { cwd: "/repo", writerRoot: "/repo" })), new RegExp(`task ${first.id}`));
187
+ assert.doesNotThrow(() => h.runner.run(writer(["lib/a.ts"], { cwd: "/repo-worktrees/feature", writerRoot: "/repo-worktrees/feature" })));
188
+ h.runner.cancelAll();
189
+ });
190
+
191
+ test("writers in different worktree roots never conflict", async () => {
192
+ const h = harness();
193
+ h.runner.run(writer(["lib/a.ts"], { cwd: "/repo" }));
194
+ assert.doesNotThrow(() => h.runner.run(writer(["lib/a.ts"], { cwd: "/repo-worktrees/feature" })));
195
+ await tick();
196
+ assert.equal(h.children.length, 2);
197
+ h.runner.cancelAll();
198
+ });
199
+
200
+ test("read-only agents are never registered and never blocked", async () => {
201
+ const h = harness();
202
+ h.runner.run(writer(["lib/**"]));
203
+ const reader = h.runner.run(writer([], { agent: explorer, prompt: "Map lib/", writerSurfaces: undefined }));
204
+ const second = h.runner.run(writer([], { agent: explorer, prompt: "Map lib/ again", writerSurfaces: undefined }));
205
+ await tick();
206
+ assert.equal(h.store.get(reader.id)?.status, TASK_STATUS.RUNNING);
207
+ assert.equal(h.store.get(second.id)?.status, TASK_STATUS.RUNNING);
208
+ assert.doesNotThrow(() => h.runner.run(writer(["tests/a.ts"])), "a reader holds no surfaces");
209
+ h.runner.cancelAll();
210
+ });