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,344 @@
1
+ import assert from "node:assert/strict";
2
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import * as fsPromises from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join, win32 } from "node:path";
6
+ import test from "node:test";
7
+ import {
8
+ CHANNEL_SCHEMA,
9
+ GENTLE_AI_MAIN_PACKAGE,
10
+ MainChannelError,
11
+ buildMainGentleAi,
12
+ channelStatePath,
13
+ installOwner,
14
+ mainChannelAdapter,
15
+ mainVersion,
16
+ packMainShell,
17
+ readChannel,
18
+ resolveMainCommit,
19
+ runUpgrade,
20
+ writeChannel,
21
+ } from "../scripts/main-channel.mjs";
22
+ import { artifactFor } from "../scripts/installer-downloads.mjs";
23
+
24
+ const SHELL_SHA = "6e7e3a18f794223396527a54c7c36d19c7d236c6";
25
+ const AI_SHA = "1f9d5e6423e37f7d2316859045f379ba9b5d8c3a";
26
+ const posixOnly = { skip: process.platform === "win32" };
27
+
28
+ function sandbox() {
29
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "main-channel-")));
30
+ const home = join(root, "home");
31
+ mkdirSync(home);
32
+ return { root, home, ctx: { env: {}, home }, cleanup: () => rmSync(root, { recursive: true, force: true }) };
33
+ }
34
+ type Call = { command: string; argv: string[]; options: { env?: Record<string, string>; cwd?: string } };
35
+
36
+ test("channel state defaults to release and round-trips a main record", async () => {
37
+ const s = sandbox();
38
+ try {
39
+ assert.equal(channelStatePath(s.ctx), join(s.home, ".pi", "gentle-ai", "channel.json"));
40
+ assert.equal(channelStatePath({ env: { GENTLE_PI_CONFIG_HOME: join(s.root, "cfg") }, home: s.home }), join(s.root, "cfg", "channel.json"));
41
+ assert.deepEqual(await readChannel(s.ctx, fsPromises), { channel: "release" });
42
+ await writeChannel(s.ctx, { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA }, fsPromises);
43
+ assert.deepEqual(JSON.parse(readFileSync(channelStatePath(s.ctx), "utf8")),
44
+ { schema: CHANNEL_SCHEMA, channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA });
45
+ assert.deepEqual(await readChannel(s.ctx, fsPromises), { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA });
46
+ } finally { s.cleanup(); }
47
+ });
48
+
49
+ test("a malformed channel state fails closed instead of falling back to release", async () => {
50
+ const s = sandbox();
51
+ try {
52
+ mkdirSync(join(s.home, ".pi", "gentle-ai"), { recursive: true });
53
+ for (const bad of ["not json", JSON.stringify({ schema: CHANNEL_SCHEMA, channel: "nightly" }),
54
+ JSON.stringify({ schema: CHANNEL_SCHEMA, channel: "main", shellCommit: "abc", gentleAiCommit: AI_SHA })]) {
55
+ writeFileSync(channelStatePath(s.ctx), bad);
56
+ await assert.rejects(readChannel(s.ctx, fsPromises), (error: MainChannelError) => error.code === "channel-state-invalid");
57
+ }
58
+ } finally { s.cleanup(); }
59
+ });
60
+
61
+ test("the latest main commit comes from GitHub as an exact 40-character SHA", async () => {
62
+ const requests: Array<{ url: string; accept: string | undefined }> = [];
63
+ const fetch = async (url: string, init: { headers: Record<string, string> }) => {
64
+ requests.push({ url, accept: init.headers.Accept });
65
+ return { ok: true, status: 200, text: async () => `${SHELL_SHA}\n` };
66
+ };
67
+ assert.equal(await resolveMainCommit("Gentleman-Programming/gentle-shell", { fetch }), SHELL_SHA);
68
+ assert.deepEqual(requests, [{ url: "https://api.github.com/repos/Gentleman-Programming/gentle-shell/commits/main", accept: "application/vnd.github.sha" }]);
69
+ for (const response of [{ ok: true, status: 200, text: async () => "abc123" }, { ok: false, status: 403, text: async () => SHELL_SHA }]) {
70
+ await assert.rejects(resolveMainCommit("Gentleman-Programming/gentle-shell", { fetch: async () => response }),
71
+ (error: MainChannelError) => error.code === "main-commit-unavailable");
72
+ }
73
+ });
74
+
75
+ test("a main version marks the release version with the commit", () => {
76
+ assert.equal(mainVersion("4.0.0", SHELL_SHA), "4.0.0-main.6e7e3a18f794");
77
+ });
78
+
79
+ test("the Gentle Shell main package is packed from the exact commit without prepack", async () => {
80
+ const s = sandbox();
81
+ try {
82
+ const calls: Call[] = [];
83
+ let downloaded = "";
84
+ let packed: { version: string; scripts: Record<string, string | undefined> } | null = null;
85
+ const fetch = async (url: string) => {
86
+ downloaded = url;
87
+ return { ok: true, status: 200, arrayBuffer: async () => new TextEncoder().encode("archive").buffer };
88
+ };
89
+ const run = async (command: string, argv: string[], options: Call["options"]) => {
90
+ calls.push({ command, argv, options });
91
+ if (argv[0] === "-xzf") {
92
+ const target = argv[argv.indexOf("-C") + 1];
93
+ writeFileSync(join(target, "package.json"), JSON.stringify({ name: "gentle-pi", version: "4.0.0",
94
+ scripts: { prepack: "pnpm test", prepare: "x", postinstall: "node scripts/install-gentle-ai.mjs" } }));
95
+ } else {
96
+ packed = JSON.parse(readFileSync(join(options.cwd!, "package.json"), "utf8"));
97
+ const destination = argv[argv.indexOf("--pack-destination") + 1];
98
+ writeFileSync(join(destination, "gentle-pi-4.0.0-main.6e7e3a18f794.tgz"), "tgz");
99
+ }
100
+ return { code: 0, stdout: "" };
101
+ };
102
+ const tgz = await packMainShell({ commit: SHELL_SHA, ctx: s.ctx, fetch, run, pnpm: { command: "/bin/pnpm", prefix: [] }, fs: fsPromises });
103
+ assert.equal(downloaded, `https://codeload.github.com/Gentleman-Programming/gentle-shell/tar.gz/${SHELL_SHA}`);
104
+ assert.equal(tgz, join(s.home, ".pi", "gentle-ai", "main", "packages", "gentle-pi-4.0.0-main.6e7e3a18f794.tgz"));
105
+ assert.ok(existsSync(tgz));
106
+ const pack = calls.find((call) => call.command === "/bin/pnpm");
107
+ assert.ok(pack);
108
+ assert.deepEqual(pack.argv.slice(0, 1), ["pack"]);
109
+ assert.ok(packed);
110
+ assert.equal(packed.version, "4.0.0-main.6e7e3a18f794");
111
+ assert.equal(packed.scripts.prepack, undefined);
112
+ assert.equal(packed.scripts.prepare, undefined);
113
+ assert.equal(packed.scripts.postinstall, "node scripts/install-gentle-ai.mjs");
114
+ assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "main", ".source")), false, "the extracted source is removed");
115
+ } finally { s.cleanup(); }
116
+ });
117
+
118
+ // Run 38061123855: with Git for Windows' usr\bin first on PATH, MSYS tar reads `D:\...` as a remote host.
119
+ test("on Windows the source archive is extracted by System32's tar.exe by absolute path; POSIX keeps tar from PATH", async () => {
120
+ const windowsTar = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
121
+ for (const [platform, expected] of [["win32", windowsTar], ["linux", "tar"], ["darwin", "tar"]]) {
122
+ const s = sandbox();
123
+ try {
124
+ const extracts: string[] = [];
125
+ const fetch = async () => ({ ok: true, status: 200, arrayBuffer: async () => new TextEncoder().encode("archive").buffer });
126
+ const run = async (command: string, argv: string[], options: Call["options"]) => {
127
+ if (argv[0] === "-xzf") {
128
+ extracts.push(command);
129
+ writeFileSync(join(argv[argv.indexOf("-C") + 1], "package.json"), JSON.stringify({ name: "gentle-pi", version: "4.0.0" }));
130
+ } else {
131
+ writeFileSync(join(argv[argv.indexOf("--pack-destination") + 1], "gentle-pi-4.0.0-main.6e7e3a18f794.tgz"), "tgz");
132
+ assert.ok(options.cwd);
133
+ }
134
+ return { code: 0, stdout: "" };
135
+ };
136
+ await packMainShell({ commit: SHELL_SHA, ctx: s.ctx, fetch, run, pnpm: { command: "/bin/pnpm", prefix: [] }, fs: fsPromises, platform });
137
+ assert.deepEqual(extracts, [expected], platform);
138
+ } finally { s.cleanup(); }
139
+ }
140
+ });
141
+
142
+ test("a failed source extraction or Go build keeps the command's bounded stderr tail as the error's cause", async () => {
143
+ const s = sandbox();
144
+ try {
145
+ const tail = "tar (child): Cannot connect to D: resolve failed\ntar: Error is not recoverable: exiting now\n";
146
+ const requested: Array<number | undefined> = [];
147
+ const fetch = async () => ({ ok: true, status: 200, arrayBuffer: async () => new TextEncoder().encode("archive").buffer });
148
+ const tar = async (_command: string, _argv: string[], options: { stderrTail?: number }) => {
149
+ requested.push(options.stderrTail);
150
+ return { code: 2, stdout: "", stderrTail: tail };
151
+ };
152
+ await assert.rejects(packMainShell({ commit: SHELL_SHA, ctx: s.ctx, fetch, run: tar, pnpm: { command: "/bin/pnpm", prefix: [] }, fs: fsPromises, platform: "linux" }),
153
+ (error: MainChannelError) => error.code === "main-shell-pack-failed" && error.cause instanceof Error && error.cause.message === tail.trim());
154
+ const go = "go: downloading github.com/gentleman-programming/gentle-ai/v4\ngo: verifying module: checksum mismatch\n";
155
+ const build = async (_command: string, _argv: string[], options: { stderrTail?: number }) => {
156
+ requested.push(options.stderrTail);
157
+ return { code: 1, stdout: "", stderrTail: go };
158
+ };
159
+ await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "linux", goPath: "/go/bin/go", run: build, fs: fsPromises }),
160
+ (error: MainChannelError) => error.code === "main-gentle-ai-build-failed" && error.cause instanceof Error && error.cause.message === go.trim());
161
+ assert.deepEqual(requested, [4096, 4096]);
162
+ // Without any stderr the error has no cause, as before.
163
+ await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "linux", goPath: "/go/bin/go", run: async () => ({ code: 1, stdout: "" }), fs: fsPromises }),
164
+ (error: MainChannelError) => error.code === "main-gentle-ai-build-failed" && error.cause === undefined);
165
+ } finally { s.cleanup(); }
166
+ });
167
+
168
+ test("Gentle AI main is built from the exact commit through Go's checksum database and registered as the override", posixOnly, async () => {
169
+ const s = sandbox();
170
+ try {
171
+ const goPath = join(s.root, "go", "bin", "go");
172
+ const calls: Call[] = [];
173
+ const run = async (command: string, argv: string[], options: Call["options"]) => {
174
+ calls.push({ command, argv, options });
175
+ if (argv[0] === "install") {
176
+ mkdirSync(options.env!.GOBIN, { recursive: true });
177
+ const binary = join(options.env!.GOBIN, "gentle-ai");
178
+ writeFileSync(binary, "#!/bin/sh\n");
179
+ chmodSync(binary, 0o755);
180
+ return { code: 0, stdout: "" };
181
+ }
182
+ return { code: 0, stdout: "gentle-ai 4.0.1-0.20261008202137-1f9d5e6423e3\n" };
183
+ };
184
+ const built = await buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath, run, fs: fsPromises });
185
+ const install = calls[0];
186
+ assert.equal(install.command, goPath);
187
+ assert.deepEqual(install.argv, ["install", `${GENTLE_AI_MAIN_PACKAGE}@${AI_SHA}`]);
188
+ for (const [key, value] of Object.entries({ GOPROXY: "https://proxy.golang.org", GOSUMDB: "sum.golang.org", GOTOOLCHAIN: "local",
189
+ GOFLAGS: "-modcacherw", GONOSUMDB: "", GOPRIVATE: "", GOINSECURE: "", GOENV: "off", GOWORK: "off" })) assert.equal(install.options.env![key], value, key);
190
+ assert.equal(built.binaryPath, join(s.home, ".pi", "gentle-ai", "main", "gentle-ai", AI_SHA, "gentle-ai"));
191
+ assert.equal(built.version, "4.0.1-0.20261008202137-1f9d5e6423e3");
192
+ assert.deepEqual(JSON.parse(readFileSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json"), "utf8")),
193
+ { schema: "gentle-pi.dev-binary/v1", path: built.binaryPath });
194
+ } finally { s.cleanup(); }
195
+ });
196
+
197
+ test("a Gentle AI build whose version names another commit is rejected and never registered", posixOnly, async () => {
198
+ const s = sandbox();
199
+ try {
200
+ const run = async (_command: string, argv: string[], options: Call["options"]) => {
201
+ if (argv[0] === "install") {
202
+ mkdirSync(options.env!.GOBIN, { recursive: true });
203
+ writeFileSync(join(options.env!.GOBIN, "gentle-ai"), "#!/bin/sh\n", { mode: 0o755 });
204
+ }
205
+ return { code: 0, stdout: "gentle-ai 4.0.1-0.20261008202137-aaaaaaaaaaaa\n" };
206
+ };
207
+ await assert.rejects(buildMainGentleAi({ commit: AI_SHA, ctx: s.ctx, platform: "darwin", goPath: "/go/bin/go", run, fs: fsPromises }),
208
+ (error: MainChannelError) => error.code === "main-gentle-ai-unverified");
209
+ assert.equal(existsSync(join(s.home, ".pi", "gentle-ai", "dev-binary.json")), false);
210
+ } finally { s.cleanup(); }
211
+ });
212
+
213
+ test("the runner adapter resolves commits and records the channel through the given network and files", async () => {
214
+ const s = sandbox();
215
+ try {
216
+ const urls: string[] = [];
217
+ const adapter = mainChannelAdapter({ fs: fsPromises, fetch: async (url: string) => {
218
+ urls.push(url);
219
+ return { ok: true, status: 200, text: async () => AI_SHA };
220
+ } });
221
+ assert.equal(await adapter.resolveCommit("Gentleman-Programming/gentle-ai"), AI_SHA);
222
+ assert.deepEqual(urls, ["https://api.github.com/repos/Gentleman-Programming/gentle-ai/commits/main"]);
223
+ await adapter.writeChannel(s.ctx, { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA });
224
+ assert.deepEqual(await readChannel(s.ctx, fsPromises), { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA });
225
+ } finally { s.cleanup(); }
226
+ });
227
+
228
+ test("the owner of an installation is pnpm under PNPM_HOME, npm only inside npm's global root, otherwise unknown", () => {
229
+ const roots = { pnpmHome: "/u/Library/pnpm", npmRoot: "/u/.local/lib/node_modules" };
230
+ assert.equal(installOwner({ ...roots, packageRoot: "/u/Library/pnpm/global/v11/x/node_modules/gentle-pi" }), "pnpm");
231
+ assert.equal(installOwner({ ...roots, packageRoot: "/u/.local/lib/node_modules/gentle-pi" }), "npm");
232
+ for (const packageRoot of ["/u/work/gentle-pi", "/u/.local/lib/node_modules", "/u/.local/lib/node_modules/other/gentle-pi"]) {
233
+ assert.equal(installOwner({ ...roots, packageRoot }), null, packageRoot);
234
+ }
235
+ assert.equal(installOwner({ pnpmHome: null, npmRoot: null, packageRoot: "/u/.local/lib/node_modules/gentle-pi" }), null);
236
+ });
237
+
238
+ test("installOwner attributes another named package, such as the scoped Pi, the same way", () => {
239
+ const roots = { pnpmHome: "/u/Library/pnpm", npmRoot: "/u/.local/lib/node_modules", name: "@earendil-works/pi-coding-agent" };
240
+ assert.equal(installOwner({ ...roots, packageRoot: "/u/.local/lib/node_modules/@earendil-works/pi-coding-agent" }), "npm");
241
+ assert.equal(installOwner({ ...roots, packageRoot: "/u/Library/pnpm/global/v11/x/node_modules/@earendil-works/pi-coding-agent" }), "pnpm");
242
+ for (const packageRoot of ["/u/.local/lib/node_modules/gentle-pi", "/u/.local/lib/node_modules/@earendil-works",
243
+ "/u/.local/share/mise/installs/pi/lib/node_modules/@earendil-works/pi-coding-agent"]) {
244
+ assert.equal(installOwner({ ...roots, packageRoot }), null, packageRoot);
245
+ }
246
+ });
247
+
248
+ test("installOwner follows Windows paths on any host: backslashes and case-insensitive roots", () => {
249
+ const roots = { pnpmHome: "C:\\Users\\u\\AppData\\Local\\pnpm", npmRoot: "C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules",
250
+ name: "@earendil-works/pi-coding-agent", platform: "win32" };
251
+ assert.equal(installOwner({ ...roots, packageRoot: "C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules\\@earendil-works\\pi-coding-agent" }), "npm");
252
+ assert.equal(installOwner({ ...roots, packageRoot: "c:\\users\\U\\appdata\\roaming\\NPM\\Node_Modules\\@Earendil-Works\\Pi-Coding-Agent" }), "npm");
253
+ assert.equal(installOwner({ ...roots, packageRoot: "C:\\Users\\u\\AppData\\Local\\PNPM\\global\\v11\\x\\node_modules\\@earendil-works\\pi-coding-agent" }), "pnpm");
254
+ for (const packageRoot of ["C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules\\other\\@earendil-works\\pi-coding-agent",
255
+ "C:\\Users\\u\\AppData\\Roaming\\npm\\node_modules", "D:\\Users\\u\\AppData\\Roaming\\npm\\node_modules\\@earendil-works\\pi-coding-agent",
256
+ "C:\\Users\\u\\AppData\\Local\\pnpm"]) {
257
+ assert.equal(installOwner({ ...roots, packageRoot }), null, packageRoot);
258
+ }
259
+ // POSIX paths keep their exact case.
260
+ assert.equal(installOwner({ pnpmHome: null, npmRoot: "/u/lib/node_modules", packageRoot: "/u/lib/node_modules/Gentle-Pi", platform: "linux" }), null);
261
+ });
262
+
263
+ // `gentle-shell upgrade --channel main` after the installer provided its own pinned Go.
264
+ const NEW_AI = "2222222222222222222222222222222222222222";
265
+ /** A main install whose Gentle Shell is current, so only Gentle AI is rebuilt with Go. */
266
+ async function upgradeWorld({ userGo = null as string | null, pinned = false, acquireFails = false }) {
267
+ const s = sandbox();
268
+ const goDirectory = join(s.home, ".pi", "gentle-ai", "tools", "go", "1.25.14");
269
+ const pinnedGo = join(goDirectory, "go", "bin", "go");
270
+ if (pinned) {
271
+ mkdirSync(join(goDirectory, "go", "bin"), { recursive: true, mode: 0o700 });
272
+ chmodSync(goDirectory, 0o700);
273
+ writeFileSync(pinnedGo, "#!/bin/sh\n", { mode: 0o755 });
274
+ writeFileSync(join(goDirectory, ".gentle-shell-go"), `${artifactFor("go", "darwin", "arm64").url}\n`);
275
+ }
276
+ await writeChannel(s.ctx, { channel: "main", shellCommit: SHELL_SHA, gentleAiCommit: AI_SHA }, fsPromises);
277
+ const calls: Call[] = [];
278
+ const fetch = async (url: string) => ({ ok: true, status: 200, text: async () => (url.endsWith("/gentle-ai/commits/main") ? NEW_AI : SHELL_SHA) });
279
+ const run = async (command: string, argv: string[], options: Call["options"]) => {
280
+ calls.push({ command, argv, options });
281
+ if (command === "/usr/bin/go" && argv[0] === "version") return { code: 0, stdout: `go version ${userGo === "devel" ? "devel go1.26-abc" : `go${userGo}`} darwin/arm64\n` };
282
+ if (argv[0] === "install") {
283
+ mkdirSync(options.env!.GOBIN, { recursive: true });
284
+ writeFileSync(join(options.env!.GOBIN, "gentle-ai"), "#!/bin/sh\n", { mode: 0o755 });
285
+ return { code: 0, stdout: "" };
286
+ }
287
+ return { code: 0, stdout: `gentle-ai 4.0.1-0.20261008202137-${NEW_AI.slice(0, 12)}\n` };
288
+ };
289
+ const which = async (name: string) => (name === "pnpm" || (name === "go" && userGo !== null) ? `/usr/bin/${name}` : null);
290
+ // The verified download itself is acquireGo's (installer-downloads tests); here it only publishes the pinned path.
291
+ const acquired: string[] = [];
292
+ const acquireGo = async () => {
293
+ acquired.push(pinnedGo);
294
+ // As acquireGo fails: wrapped, with the reason as its cause.
295
+ if (acquireFails) throw new Error("Go verified acquisition failed; nothing was published", { cause: new Error("Conflicting Go destination: /x") });
296
+ return { goPath: pinnedGo, version: "1.25.14", acquired: true };
297
+ };
298
+ const upgrade = () => runUpgrade({ args: ["--channel", "main"], ctx: s.ctx, platform: "darwin", arch: "arm64", packageRoot: join(s.root, "pkg"),
299
+ currentVersion: "4.0.0-main.6e7e3a18f794", adapters: { fetch, run, fs: fsPromises, which, acquireGo }, out: () => {} });
300
+ const builtWith = () => calls.find((call) => call.argv[0] === "install")?.command;
301
+ return { ...s, calls, pinnedGo, upgrade, builtWith, acquired };
302
+ }
303
+
304
+ test("upgrading main uses the installer's pinned Go when the user's Go is missing, older or cannot be checked, and leaves the user's Go as it is", posixOnly, async () => {
305
+ for (const userGo of [null, "1.24.0", "devel"]) {
306
+ const w = await upgradeWorld({ userGo, pinned: true });
307
+ try {
308
+ assert.equal(await w.upgrade(), 0);
309
+ assert.equal(w.builtWith(), w.pinnedGo, String(userGo));
310
+ // The user's Go only reported its version.
311
+ assert.deepEqual(w.calls.filter((call) => call.command === "/usr/bin/go").map((call) => call.argv), userGo ? [["version"]] : []);
312
+ assert.deepEqual(w.acquired, [], "an already published pinned Go is reused");
313
+ } finally { w.cleanup(); }
314
+ }
315
+ });
316
+
317
+ test("upgrading main keeps using a user's Go that meets the requirement, even next to the pinned Go", posixOnly, async () => {
318
+ const w = await upgradeWorld({ userGo: "1.26.0", pinned: true });
319
+ try {
320
+ assert.equal(await w.upgrade(), 0);
321
+ assert.equal(w.builtWith(), "/usr/bin/go");
322
+ } finally { w.cleanup(); }
323
+ });
324
+
325
+ test("upgrading main without a usable Go downloads the installer's pinned Go once, only to build", posixOnly, async () => {
326
+ for (const userGo of [null, "1.24.0", "devel"]) {
327
+ const w = await upgradeWorld({ userGo });
328
+ try {
329
+ assert.equal(await w.upgrade(), 0);
330
+ assert.deepEqual(w.acquired, [w.pinnedGo], String(userGo));
331
+ assert.equal(w.builtWith(), w.pinnedGo, String(userGo));
332
+ assert.deepEqual(w.calls.filter((call) => call.command === "/usr/bin/go").map((call) => call.argv), userGo ? [["version"]] : []);
333
+ } finally { w.cleanup(); }
334
+ }
335
+ });
336
+
337
+ test("upgrading main stops before building when the pinned Go cannot be downloaded", posixOnly, async () => {
338
+ const w = await upgradeWorld({ userGo: "devel", acquireFails: true });
339
+ try {
340
+ await assert.rejects(w.upgrade(), (error: MainChannelError) => error.code === "main-requires-tools" &&
341
+ error.message.includes("Go 1.25.14") && error.message.includes("Conflicting Go destination: /x") && !error.message.includes("network"));
342
+ assert.equal(w.builtWith(), undefined);
343
+ } finally { w.cleanup(); }
344
+ });
@@ -66,8 +66,11 @@ function descriptor(overrides = {}) {
66
66
  ...overrides,
67
67
  };
68
68
  }
69
+ function isRoleError(error: unknown): error is Error & { kind: string; stage: string; mutationOutcome: string } {
70
+ return error instanceof Error && error instanceof ProviderRoleVectorError && "kind" in error && typeof error.kind === "string" && "stage" in error && typeof error.stage === "string" && "mutationOutcome" in error && typeof error.mutationOutcome === "string";
71
+ }
69
72
  function rejects(obj, fragment) {
70
- assert.throws(() => validateDescriptor(obj), (error) => error instanceof DescriptorValidationError && error.message.includes(fragment));
73
+ assert.throws(() => validateDescriptor(obj), (error) => error instanceof Error && error instanceof DescriptorValidationError && error.message.includes(fragment));
71
74
  }
72
75
  function tempDescriptor(t, obj) {
73
76
  const directory = mkdtempSync(join(tmpdir(), "gentle-pi-maintainer-"));
@@ -102,11 +105,9 @@ test("rejects malformed descriptor fields with exact-shape errors (no defaults,
102
105
  rejects({ ...descriptor(), cases: [{ ...descriptor().cases[0]!, captureArgumentTokens: [...CAPTURE_TOKENS].filter((t) => t !== "--materialize=true") }] }, "--materialize=true");
103
106
  });
104
107
  test("submission is validated through the real relay resolver before any process launches (exact shape)", () => {
105
- const twoValues = structuredClone(SUBMISSION);
106
- twoValues.values = [...SUBMISSION.values, { slot: "extra", domain: "artifact_path_or_stdin", substitutionLocation: 0 }];
108
+ const twoValues = { ...structuredClone(SUBMISSION), values: [...SUBMISSION.values, { slot: "extra", domain: "artifact_path_or_stdin", substitutionLocation: 0 }] };
107
109
  rejects({ ...descriptor(), cases: [{ ...descriptor().cases[0]!, submission: twoValues }] }, "not a bindable provider form");
108
- const noSlot = structuredClone(SUBMISSION);
109
- noSlot.argumentTokens = [...BINDING_TOKENS, "--input=/no/value/slot"];
110
+ const noSlot = { ...structuredClone(SUBMISSION), argumentTokens: [...BINDING_TOKENS, "--input=/no/value/slot"] };
110
111
  rejects({ ...descriptor(), cases: [{ ...descriptor().cases[0]!, submission: noSlot }] }, "not a bindable provider form");
111
112
  rejects({ ...descriptor(), cases: [{ ...descriptor().cases[0]!, submission: { ...structuredClone(SUBMISSION), extra: 1 } }] }, "extra");
112
113
  });
@@ -245,7 +246,7 @@ test("negative control: a role vector failure (not surface-unavailable) is repor
245
246
  });
246
247
  test("runProviderRoleVector treats a prelaunch abort as not-started with no mutation", async () => {
247
248
  const controller = new AbortController(); controller.abort();
248
- await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: process.execPath, signal: controller.signal }), (error) => error instanceof ProviderRoleVectorError && error.kind === ROLE_VECTOR_FAILURE.ROLE_ABORTED && error.stage === "launch" && error.mutationOutcome === "none");
249
+ await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: process.execPath, signal: controller.signal }), (error) => isRoleError(error) && error.kind === ROLE_VECTOR_FAILURE.ROLE_ABORTED && error.stage === "launch" && error.mutationOutcome === "none");
249
250
  });
250
251
  function roleChild(t: test.TestContext, name: string, source: string) {
251
252
  const path = sandboxPath(`${name} preload.cjs`), savedNodeOptions = process.env.NODE_OPTIONS; writeFileSync(path, `const roleArgv = ["review", ...process.argv.slice(2)], Module = require("node:module"), resolveFilename = Module._resolveFilename; Module._resolveFilename = function (request, ...args) { return /[\\\\/]review$/.test(request) ? ${JSON.stringify(path)} : resolveFilename.call(this, request, ...args); }; ${source}`);
@@ -262,19 +263,19 @@ test("runProviderRoleVector executes both real stub-child verbs and decodes snak
262
263
  test("runProviderRoleVector rejects malformed artifact binding shape before stale binding comparison", async (t) => {
263
264
  for (const malformed of [{ lineage_id: "", target_identity: ROLE_TARGET }, { lineage_id: ROLE_LINEAGE, target_identity: "not-a-sha256" }]) {
264
265
  const child = roleChild(t, `malformed-${malformed.lineage_id || "lineage"}.cjs`, `process.stdout.write(JSON.stringify({ schema: ${JSON.stringify(PROVIDER_ROLE_CAPTURE_ARTIFACT_SCHEMA)}, role: "refuter", captured: true, ...${JSON.stringify(malformed)} })); process.exit(0);`);
265
- await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env }), (error) => error instanceof ProviderRoleVectorError && error.kind === ROLE_VECTOR_FAILURE.ROLE_FAILED && /typed shape/.test(error.message));
266
+ await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env }), (error) => isRoleError(error) && error.kind === ROLE_VECTOR_FAILURE.ROLE_FAILED && /typed shape/.test(error.message));
266
267
  }
267
268
  });
268
269
  test("runProviderRoleVector classifies an empty successful artifact as unknown and requires STATUS re-query", async (t) => {
269
270
  const child = roleChild(t, "empty-artifact.cjs", "process.exit(0);");
270
271
  await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env }), (caught) => {
271
- assert.ok(caught instanceof ProviderRoleVectorError); assert.equal(caught.kind, ROLE_VECTOR_FAILURE.EMPTY_ARTIFACT); assert.equal(caught.stage, "execute"); assert.equal(caught.mutationOutcome, "unknown"); assert.match(caught.message, /re-query negotiated STATUS before any retry/); return true;
272
+ assert.ok(isRoleError(caught)); assert.equal(caught.kind, ROLE_VECTOR_FAILURE.EMPTY_ARTIFACT); assert.equal(caught.stage, "execute"); assert.equal(caught.mutationOutcome, "unknown"); assert.match(caught.message, /re-query negotiated STATUS before any retry/); return true;
272
273
  });
273
274
  });
274
275
  test("runProviderRoleVector bounds each output stream before JSON or exit handling", async (t) => {
275
276
  for (const stream of ["stdout", "stderr"] as const) {
276
277
  const child = roleChild(t, `overflow-${stream}.cjs`, `process.${stream}.write("x".repeat(${ROLE_STREAM_MAX_BYTES + 1})); setInterval(() => {}, 1_000);`);
277
- await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env, timeoutMs: 1_000 }), (error) => error instanceof ProviderRoleVectorError && error.kind === ROLE_VECTOR_FAILURE.ROLE_OUTPUT_OVERFLOW && error.mutationOutcome === "unknown");
278
+ await assert.rejects(runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env, timeoutMs: 1_000 }), (error) => isRoleError(error) && error.kind === ROLE_VECTOR_FAILURE.ROLE_OUTPUT_OVERFLOW && error.mutationOutcome === "unknown");
278
279
  }
279
280
  });
280
281
  async function descendantPid(path: string) {
@@ -293,7 +294,7 @@ test("role watchdog and abort reap descendants on every platform", async (t) =>
293
294
  t.after(() => { if (pid > 0) try { process.kill(pid, "SIGKILL"); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; } });
294
295
  assert.ok(DEFAULT_ROLE_VECTOR_TIMEOUT_MS > 600_000, "the outer 900s watchdog FIRES after the provider-owned 600s deadline with setup/cancellation margin");
295
296
  if (mode === "abort") controller.abort();
296
- await assert.rejects(run, (error) => error instanceof ProviderRoleVectorError && error.kind === (mode === "abort" ? ROLE_VECTOR_FAILURE.ROLE_ABORTED : ROLE_VECTOR_FAILURE.ROLE_TIMED_OUT) && error.mutationOutcome === "unknown");
297
+ await assert.rejects(run, (error) => isRoleError(error) && error.kind === (mode === "abort" ? ROLE_VECTOR_FAILURE.ROLE_ABORTED : ROLE_VECTOR_FAILURE.ROLE_TIMED_OUT) && error.mutationOutcome === "unknown");
297
298
  await assertReaped(pid);
298
299
  });
299
300
  });
@@ -302,7 +303,7 @@ test("failed tree termination settles despite a surviving descendant retaining i
302
303
  const child = roleChild(t, "pipe-holder-role-child.cjs", `const fs = require("node:fs"), { spawn } = require("node:child_process"), grandchild = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "inherit", env: { ...process.env, NODE_OPTIONS: "" } }); fs.writeFileSync(${JSON.stringify(sandboxPath("ROLE_PID_PLACEHOLDER"))}.replace("ROLE_PID_PLACEHOLDER", ${JSON.stringify("role-pipe-holder.pid")}), String(grandchild.pid)); setInterval(() => {}, 1000);`);
303
304
  const run = runProviderRoleVector({ kind: "provider-role-refuter", argumentTokens: REFUTER_TOKENS, gentleAiExecutable: child.executable, env: child.env, signal: controller.signal, terminateProcessTree: (process) => { process.kill("SIGKILL"); return false; } });
304
305
  const pid = await descendantPid(grandchild); t.after(() => { try { process.kill(pid, "SIGKILL"); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; } });
305
- const started = Date.now(); controller.abort(); await assert.rejects(run, (error) => error instanceof ProviderRoleVectorError && error.kind === ROLE_VECTOR_FAILURE.ROLE_TERMINATION_FAILED && error.stage === "execute" && error.mutationOutcome === "unknown");
306
+ const started = Date.now(); controller.abort(); await assert.rejects(run, (error) => isRoleError(error) && error.kind === ROLE_VECTOR_FAILURE.ROLE_TERMINATION_FAILED && error.stage === "execute" && error.mutationOutcome === "unknown");
306
307
  assert.ok(Date.now() - started < 500, "failed tree termination must not wait for descendant-held pipes to close"); assert.doesNotThrow(() => process.kill(pid, 0), "the fixture descendant must survive while retaining inherited pipes");
307
308
  });
308
309
  test("stale-target fail-closed: missing or duplicate required binding tokens are rejected before runner invocation", () => {
@@ -495,7 +496,7 @@ const baselineArmed = typeof baselineBinary === "string" && baselineBinary.lengt
495
496
  const baselineReason = () => `${BASELINE_ENV} is unset or not an existing absolute path; supply the baseline gentle-ai binary (external evidence establishes whether it is RC8), or set ${REQUIRE_ENV}=1 to fail instead of skip.`;
496
497
  if (!baselineArmed && armed) throw new Error(baselineReason());
497
498
  if (!baselineArmed) console.log(`tests/maintainer/provider-relay.maintest.ts: ${baselineReason()}`);
498
- function baselineDescriptor(kind = "relay-unavailable" as const) {
499
+ function baselineDescriptor(kind: "relay-unavailable" | "positive-lens" = "relay-unavailable") {
499
500
  return validateDescriptor({ ...descriptor(), gentleAiExecutable: baselineBinary, cases: [{ name: "baseline-negative-control", kind, captureArgumentTokens: [...CAPTURE_TOKENS], submission: structuredClone(SUBMISSION) }] });
500
501
  }
501
502
  test("negative control: runMatrix proves the baseline fails closed as relay-unavailable with zero mutation", { skip: !baselineArmed }, async () => {
@@ -0,0 +1,116 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { computeModelPriceRatio, modelRoutingLine, renderModelRoutingLine, type ModelCost, type ModelPriceLookup } from "../lib/model-price-ratio.ts";
4
+
5
+ // gentle-shell#1731 S3/AC3: the runtime computes the orchestrator/worker price
6
+ // ratio from catalog prices and renders it as one fact line; the model never
7
+ // estimates it.
8
+
9
+ const catalog: Record<string, ModelCost> = {
10
+ "anthropic/fable": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
11
+ "anthropic/opus": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
12
+ "openai/mixed": { input: 3, output: 10, cacheRead: 0.3, cacheWrite: 0 },
13
+ "local/free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
14
+ "odd/thirds": { input: 9, output: 9, cacheRead: 0, cacheWrite: 0 },
15
+ "odd/three": { input: 3, output: 3, cacheRead: 0, cacheWrite: 0 },
16
+ "odd/seven": { input: 7, output: 7, cacheRead: 0, cacheWrite: 0 },
17
+ };
18
+ const lookup: ModelPriceLookup = (model) => catalog[`${model.provider}/${model.id}`];
19
+ const fable = { provider: "anthropic", id: "fable" };
20
+ const opus = { provider: "anthropic", id: "opus" };
21
+
22
+ test("renders the input, output, and blended ratio for an expensive orchestrator", () => {
23
+ assert.equal(
24
+ modelRoutingLine(fable, opus, lookup),
25
+ "Model routing: orchestrator anthropic/fable costs 3.0x the worker anthropic/opus (input 3.0x, output 3.0x).",
26
+ );
27
+ const ratio = computeModelPriceRatio(fable, opus, lookup);
28
+ assert.equal(ratio.kind, "known");
29
+ if (ratio.kind === "known") {
30
+ assert.equal(ratio.input, 3);
31
+ assert.equal(ratio.output, 3);
32
+ assert.equal(ratio.blended, 3);
33
+ }
34
+ });
35
+
36
+ test("the blended ratio is the larger of the input and output ratios", () => {
37
+ // fable/mixed: input 15/3 = 5x, output 75/10 = 7.5x.
38
+ assert.equal(
39
+ modelRoutingLine(fable, { provider: "openai", id: "mixed" }, lookup),
40
+ "Model routing: orchestrator anthropic/fable costs 7.5x the worker openai/mixed (input 5.0x, output 7.5x).",
41
+ );
42
+ });
43
+
44
+ test("the same model renders 1.0x", () => {
45
+ assert.equal(
46
+ modelRoutingLine(opus, opus, lookup),
47
+ "Model routing: orchestrator anthropic/opus costs 1.0x the worker anthropic/opus (input 1.0x, output 1.0x).",
48
+ );
49
+ });
50
+
51
+ test("a cheaper orchestrator renders a ratio below 1 normally", () => {
52
+ assert.equal(
53
+ modelRoutingLine(opus, fable, lookup),
54
+ "Model routing: orchestrator anthropic/opus costs 0.3x the worker anthropic/fable (input 0.3x, output 0.3x).",
55
+ );
56
+ });
57
+
58
+ test("ratios round to one decimal", () => {
59
+ // 7/3 = 2.333..., 9/7 = 1.2857...
60
+ assert.equal(
61
+ modelRoutingLine({ provider: "odd", id: "seven" }, { provider: "odd", id: "three" }, lookup),
62
+ "Model routing: orchestrator odd/seven costs 2.3x the worker odd/three (input 2.3x, output 2.3x).",
63
+ );
64
+ assert.equal(
65
+ modelRoutingLine({ provider: "odd", id: "thirds" }, { provider: "odd", id: "seven" }, lookup),
66
+ "Model routing: orchestrator odd/thirds costs 1.3x the worker odd/seven (input 1.3x, output 1.3x).",
67
+ );
68
+ assert.equal(renderModelRoutingLine({ kind: "known", orchestrator: "a/x", worker: "b/y", input: 2.95, output: 1.04, blended: 2.95 }),
69
+ "Model routing: orchestrator a/x costs 3.0x the worker b/y (input 3.0x, output 1.0x).");
70
+ });
71
+
72
+ test("a provider-less model reference renders its bare id", () => {
73
+ const bare: ModelPriceLookup = (model) => (model.id === "opus" ? catalog["anthropic/opus"] : undefined);
74
+ assert.equal(
75
+ modelRoutingLine({ provider: undefined, id: "opus" }, { provider: undefined, id: "opus" }, bare),
76
+ "Model routing: orchestrator opus costs 1.0x the worker opus (input 1.0x, output 1.0x).",
77
+ );
78
+ });
79
+
80
+ test("a missing orchestrator or worker model renders the unknown line", () => {
81
+ assert.equal(modelRoutingLine(undefined, opus, lookup), "Model routing: price ratio unknown (orchestrator model unknown).");
82
+ assert.equal(modelRoutingLine(fable, undefined, lookup), "Model routing: price ratio unknown (worker model unknown).");
83
+ });
84
+
85
+ test("a missing catalog price renders the unknown line naming the model", () => {
86
+ assert.equal(
87
+ modelRoutingLine(fable, { provider: "x", id: "unpriced" }, lookup),
88
+ "Model routing: price ratio unknown (no catalog price for x/unpriced).",
89
+ );
90
+ assert.equal(
91
+ modelRoutingLine({ provider: "x", id: "unpriced" }, opus, lookup),
92
+ "Model routing: price ratio unknown (no catalog price for x/unpriced).",
93
+ );
94
+ });
95
+
96
+ test("a zero or invalid input or output price renders the unknown line", () => {
97
+ assert.equal(
98
+ modelRoutingLine(fable, { provider: "local", id: "free" }, lookup),
99
+ "Model routing: price ratio unknown (zero or invalid price for local/free).",
100
+ );
101
+ assert.equal(
102
+ modelRoutingLine({ provider: "local", id: "free" }, opus, lookup),
103
+ "Model routing: price ratio unknown (zero or invalid price for local/free).",
104
+ );
105
+ const broken: ModelPriceLookup = (model) => (model.id === "opus" ? { input: Number.NaN, output: 25, cacheRead: 0, cacheWrite: 0 } : catalog["anthropic/fable"]);
106
+ assert.equal(modelRoutingLine(fable, opus, broken), "Model routing: price ratio unknown (zero or invalid price for anthropic/opus).");
107
+ const negative: ModelPriceLookup = (model) => (model.id === "opus" ? { input: 5, output: -1, cacheRead: 0, cacheWrite: 0 } : catalog["anthropic/fable"]);
108
+ assert.equal(modelRoutingLine(fable, opus, negative), "Model routing: price ratio unknown (zero or invalid price for anthropic/opus).");
109
+ });
110
+
111
+ test("a throwing price lookup never throws and renders the unknown line", () => {
112
+ const throwing: ModelPriceLookup = () => {
113
+ throw new Error("registry offline");
114
+ };
115
+ assert.equal(modelRoutingLine(fable, opus, throwing), "Model routing: price ratio unknown (price lookup failed).");
116
+ });