gentle-pi 3.7.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,481 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { RefreshModelsContext } from "@earendil-works/pi-ai";
4
+ import { clampThinkingLevel, createModels, getSupportedThinkingLevels, InMemoryCredentialStore } from "@earendil-works/pi-ai";
5
+ import type { Provider } from "@earendil-works/pi-ai";
6
+ import nanProviderExtension from "../extensions/nan-provider.ts";
7
+ import { createNanProviderConfig as createNativeProvider, isNanAdaptiveReasoningModel, NAN_PROVIDER_BASE_URL, NAN_PROVIDER_ID } from "../lib/nan-provider.ts";
8
+
9
+ // Keep catalog assertions independent of the native refresh's void return contract.
10
+ function createNanProviderConfig(options: Parameters<typeof createNativeProvider>[0] = {}) {
11
+ const provider = createNativeProvider(options);
12
+ return {
13
+ ...provider,
14
+ models: provider.getModels(),
15
+ async refreshModels(context: RefreshModelsContext) {
16
+ await provider.refreshModels!(context);
17
+ return provider.getModels();
18
+ },
19
+ };
20
+ }
21
+
22
+ const DOCUMENTED_CHAT_IDS = [
23
+ "glm5.3", "deepseek-v4-flash", "glm5.3-flash", "qwen3.8-flash",
24
+ "mimo-v2.6-flash", "gemma4", "qwen3.6",
25
+ ];
26
+
27
+ function jsonResponse(body: unknown, status = 200): Response {
28
+ return new Response(JSON.stringify(body), {
29
+ status,
30
+ headers: { "content-type": "application/json" },
31
+ });
32
+ }
33
+
34
+ function refreshContext(credential?: RefreshModelsContext["credential"]): RefreshModelsContext {
35
+ return {
36
+ credential,
37
+ stored: undefined,
38
+ allowNetwork: true,
39
+ signal: new AbortController().signal,
40
+ async publish(publication) {
41
+ publication.update?.();
42
+ return true;
43
+ },
44
+ };
45
+ }
46
+
47
+ test("native login rejects an explicitly submitted empty key", async () => {
48
+ const provider = createNativeProvider();
49
+ await assert.rejects(async () => provider.auth.apiKey!.login!({
50
+ signal: new AbortController().signal,
51
+ prompt: async () => "",
52
+ notify() {},
53
+ }), /non-empty/);
54
+ });
55
+
56
+ test("extension registers NaN with Pi's OpenAI-compatible and native API-key configuration", () => {
57
+ let registeredConfig: Provider | undefined;
58
+ nanProviderExtension({
59
+ registerProvider(provider: Provider) { registeredConfig = provider; },
60
+ } as never);
61
+ assert.equal(registeredConfig?.id, NAN_PROVIDER_ID);
62
+ assert.equal(registeredConfig?.name, "NaN");
63
+ assert.equal(registeredConfig?.baseUrl, NAN_PROVIDER_BASE_URL);
64
+ assert.equal(registeredConfig?.getModels()[0]?.api, "openai-completions");
65
+ assert.equal(typeof registeredConfig?.auth.apiKey?.login, "function");
66
+ assert.equal(typeof registeredConfig?.streamSimple, "function");
67
+ assert.equal(typeof registeredConfig?.refreshModels, "function");
68
+ });
69
+
70
+ test("native login awaits input, trims keys, and rejects whitespace or cancellation", async () => {
71
+ const login = createNativeProvider().auth.apiKey!.login!;
72
+ const controller = new AbortController();
73
+ let submit!: (key: string) => void;
74
+ let prompted = false;
75
+ let finished = false;
76
+ const pending = login({ signal: controller.signal, notify() {}, prompt: async (prompt) => {
77
+ prompted = true;
78
+ assert.equal(prompt.type, "secret");
79
+ assert.equal(prompt.signal, controller.signal);
80
+ return new Promise<string>((resolve) => { submit = resolve; });
81
+ } }).then((result) => { finished = true; return result; });
82
+ assert.equal(prompted, true);
83
+ assert.equal(finished, false);
84
+ submit(" synthetic-key \t");
85
+ assert.deepEqual(await pending, { type: "api_key", key: "synthetic-key" });
86
+ await assert.rejects(login({ signal: controller.signal, notify() {}, prompt: async () => " \t " }), /non-empty/);
87
+ await assert.rejects(login({ signal: controller.signal, notify() {}, prompt: async () => { throw new Error("cancelled"); } }), /cancelled/);
88
+ await assert.rejects(login({ signal: controller.signal, notify() {}, prompt: async () => {
89
+ controller.abort(); return "synthetic-key";
90
+ } }), { name: "AbortError" });
91
+ let calls = 0;
92
+ await assert.rejects(login({ signal: controller.signal, notify() {}, prompt: async () => { calls++; return "key"; } }), { name: "AbortError" });
93
+ assert.equal(calls, 0);
94
+ });
95
+
96
+ test("native Models login never persists blank credentials and uses saved auth for requests", async () => {
97
+ const credentials = new InMemoryCredentialStore();
98
+ const models = createModels({ credentials, authContext: {
99
+ env: async () => "env-key", fileExists: async () => false,
100
+ } });
101
+ models.setProvider(createNativeProvider());
102
+ const interaction = { prompt: async () => "", notify() {} };
103
+ await assert.rejects(models.login("nan", "api_key", interaction), /non-empty/);
104
+ assert.equal(await credentials.read("nan"), undefined);
105
+ await models.login("nan", "api_key", { ...interaction, prompt: async () => " saved-key " });
106
+ assert.deepEqual((await models.getAuth("nan"))?.auth, { apiKey: "saved-key" });
107
+ await assert.rejects(models.login("nan", "api_key", interaction), /non-empty/);
108
+ assert.deepEqual(await credentials.read("nan"), { type: "api_key", key: "saved-key" });
109
+ await models.logout("nan");
110
+ assert.deepEqual((await models.getAuth("nan"))?.auth, { apiKey: "env-key" });
111
+ });
112
+
113
+ test("native auth resolves stored keys before environment and rejects empty configuration", async () => {
114
+ const resolve = createNativeProvider().auth.apiKey!.resolve;
115
+ let envCalls = 0;
116
+ const input = {
117
+ ctx: { async env(name: string) { envCalls++; assert.equal(name, "NAN_API_KEY"); return " env-key "; }, async fileExists() { return false; } },
118
+ signal: new AbortController().signal,
119
+ };
120
+ assert.deepEqual(await resolve({ ...input, credential: { type: "api_key", key: " stored-key " } }), { auth: { apiKey: "stored-key" }, source: "API key" });
121
+ assert.equal(envCalls, 0);
122
+ assert.deepEqual(await resolve({ ...input, credential: { type: "api_key", key: " " } }), { auth: { apiKey: "env-key" }, source: "NAN_API_KEY" });
123
+ assert.equal(await resolve({ ...input, ctx: { ...input.ctx, env: async () => " " } }), undefined);
124
+ });
125
+
126
+ test("native publication exposes the new catalog synchronously, including an empty catalog", async () => {
127
+ for (const ids of [["glm5.3"], []]) {
128
+ const provider = createNativeProvider({
129
+ fetchImpl: async () => jsonResponse({ data: ids.map((id) => ({ id })) }),
130
+ });
131
+ let publications = 0;
132
+ await provider.refreshModels!({
133
+ ...refreshContext({ type: "api_key", key: "snapshot-key" }),
134
+ async publish(publication) {
135
+ publications++;
136
+ publication.update?.();
137
+ assert.deepEqual(provider.getModels().map((model) => model.id), ids);
138
+ return true;
139
+ },
140
+ });
141
+ assert.equal(publications, 1);
142
+ }
143
+ });
144
+
145
+ test("stale and aborted publication updates cannot replace the current key's catalog", async () => {
146
+ for (const mode of ["changed-key", "aborted"] as const) {
147
+ const provider = createNativeProvider({
148
+ fetchImpl: async () => jsonResponse({ data: [{ id: "glm5.3" }] }),
149
+ });
150
+ const controller = new AbortController();
151
+ await provider.refreshModels!({
152
+ ...refreshContext({ type: "api_key", key: "old-key" }), signal: controller.signal,
153
+ async publish(publication) {
154
+ if (mode === "changed-key") {
155
+ await provider.refreshModels!({
156
+ ...refreshContext({ type: "api_key", key: "new-key" }), allowNetwork: false,
157
+ publish: async () => { assert.fail("offline refresh must not publish"); },
158
+ });
159
+ } else controller.abort();
160
+ publication.update?.();
161
+ assert.deepEqual(provider.getModels().map((model) => model.id), DOCUMENTED_CHAT_IDS);
162
+ return false;
163
+ },
164
+ });
165
+ assert.deepEqual(provider.getModels().map((model) => model.id), DOCUMENTED_CHAT_IDS);
166
+ }
167
+ });
168
+
169
+ test("offline and already-aborted refreshes invalidate keys without publication", async () => {
170
+ const provider = createNativeProvider({ fetchImpl: async () => jsonResponse({ data: [{ id: "glm5.3" }] }) });
171
+ await provider.refreshModels!(refreshContext({ type: "api_key", key: "first" }));
172
+ for (const mode of ["offline", "aborted"] as const) {
173
+ const controller = new AbortController();
174
+ if (mode === "aborted") controller.abort();
175
+ await provider.refreshModels!({
176
+ ...refreshContext({ type: "api_key", key: mode }),
177
+ allowNetwork: mode !== "offline", signal: controller.signal,
178
+ publish: async () => { assert.fail("refresh must not publish"); },
179
+ });
180
+ assert.deepEqual(provider.getModels().map((model) => model.id), DOCUMENTED_CHAT_IDS);
181
+ }
182
+ });
183
+
184
+ test("initial catalog contains all seven documented chat models with configured output caps", () => {
185
+ const models = createNanProviderConfig().models;
186
+ assert.deepEqual(models.map((model) => model.id), DOCUMENTED_CHAT_IDS);
187
+ const model = models.find((model) => model.id === "deepseek-v4-flash");
188
+ assert.equal(model?.id, "deepseek-v4-flash");
189
+ assert.equal(model?.api, "openai-completions");
190
+ assert.equal(model?.reasoning, true);
191
+ assert.deepEqual(model?.input, ["text", "image"]);
192
+ assert.equal(model?.contextWindow, 1_000_000);
193
+ assert.equal(models.length, 7);
194
+ assert.equal(model?.maxTokens, 16_384);
195
+ assert.deepEqual(model?.cost, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
196
+ });
197
+
198
+ test("GLM models expose exactly NaN's four efforts and no off", () => {
199
+ const models = createNativeProvider().getModels();
200
+ for (const id of ["glm5.3", "glm5.3-flash"]) {
201
+ const model = models.find((model) => model.id === id);
202
+ assert.ok(model);
203
+ assert.deepEqual(model.thinkingLevelMap, { off: null, minimal: null, xhigh: null, max: "max" });
204
+ assert.deepEqual(getSupportedThinkingLevels(model), ["low", "medium", "high", "max"]);
205
+ // Pi levels NaN does not declare clamp onto the nearest declared effort.
206
+ assert.equal(clampThinkingLevel(model, "off"), "low");
207
+ assert.equal(clampThinkingLevel(model, "minimal"), "low");
208
+ assert.equal(clampThinkingLevel(model, "xhigh"), "max");
209
+ }
210
+ });
211
+
212
+ test("models with none and minimal expose exactly NaN's six efforts", () => {
213
+ const models = createNativeProvider().getModels();
214
+ // Qwen 3.6 and Gemma 4 honor these values as depth controls; DeepSeek V4 Flash,
215
+ // Qwen 3.8 Flash, and MiMo accept them while NaN manages depth itself.
216
+ for (const id of ["qwen3.6", "gemma4", "deepseek-v4-flash", "qwen3.8-flash", "mimo-v2.6-flash"]) {
217
+ const model = models.find((model) => model.id === id);
218
+ assert.ok(model);
219
+ assert.deepEqual(model.thinkingLevelMap, { off: "none", xhigh: null, max: "max" });
220
+ assert.deepEqual(getSupportedThinkingLevels(model), ["off", "minimal", "low", "medium", "high", "max"]);
221
+ assert.equal(clampThinkingLevel(model, "xhigh"), "max");
222
+ }
223
+ });
224
+
225
+ test("only the self-managing models carry the adaptive reasoning marker", () => {
226
+ const models = createNativeProvider().getModels();
227
+ assert.deepEqual(
228
+ models.filter((model) => isNanAdaptiveReasoningModel(model)).map((model) => model.id),
229
+ ["deepseek-v4-flash", "qwen3.8-flash", "mimo-v2.6-flash"],
230
+ );
231
+ // Qwen 3.6 and Gemma 4 share the same map but honor the level as a depth budget.
232
+ for (const id of ["qwen3.6", "gemma4", "glm5.3", "glm5.3-flash"]) {
233
+ const model = models.find((model) => model.id === id);
234
+ assert.ok(model);
235
+ assert.equal(isNanAdaptiveReasoningModel(model), false);
236
+ }
237
+ assert.equal(isNanAdaptiveReasoningModel(undefined), false);
238
+ assert.equal(isNanAdaptiveReasoningModel({ provider: "openai", id: "deepseek-v4-flash" }), false);
239
+ });
240
+
241
+ test("thinking maps are isolated across snapshots, providers, and live catalog refreshes", async () => {
242
+ const provider = createNativeProvider({
243
+ fetchImpl: async () => jsonResponse({ data: DOCUMENTED_CHAT_IDS.map((id) => ({ id })) }),
244
+ });
245
+ const baseline = provider.getModels();
246
+ for (const model of provider.getModels()) {
247
+ assert.ok(model.thinkingLevelMap);
248
+ model.thinkingLevelMap.off = "mutated";
249
+ model.thinkingLevelMap.medium = null;
250
+ }
251
+ assert.deepEqual(provider.getModels(), baseline);
252
+ assert.deepEqual(createNativeProvider().getModels(), baseline);
253
+ await provider.refreshModels!(refreshContext({ type: "api_key", key: "map-key" }));
254
+ assert.deepEqual(provider.getModels(), baseline);
255
+ for (const model of provider.getModels()) {
256
+ assert.ok(model.thinkingLevelMap);
257
+ model.thinkingLevelMap.xhigh = "mutated";
258
+ }
259
+ assert.deepEqual(provider.getModels(), baseline);
260
+ await provider.refreshModels!(refreshContext({ type: "api_key", key: "map-key" }));
261
+ assert.deepEqual(provider.getModels(), baseline);
262
+ });
263
+
264
+ test("catalog snapshots cannot mutate the offline baseline or another provider", async () => {
265
+ const provider = createNativeProvider();
266
+ const snapshot = [...provider.getModels()];
267
+ snapshot[0].id = "mutated";
268
+ snapshot[0].input.push("image");
269
+ snapshot[0].cost.input = 99;
270
+ snapshot.pop();
271
+ const fresh = provider.getModels();
272
+ assert.deepEqual(fresh.map((model) => model.id), DOCUMENTED_CHAT_IDS);
273
+ assert.deepEqual(fresh[0].input, ["text", "image"]);
274
+ assert.equal(fresh[0].cost.input, 0);
275
+ assert.deepEqual(createNativeProvider().getModels(), fresh);
276
+ await provider.refreshModels!({
277
+ ...refreshContext({ type: "api_key", key: "changed-key" }), allowNetwork: false,
278
+ });
279
+ assert.deepEqual(provider.getModels(), fresh);
280
+ });
281
+
282
+ test("live discovery uses the key-scoped endpoint and replaces the fallback with listed models", async () => {
283
+ let request: { url: string; init?: RequestInit } | undefined;
284
+ const config = createNanProviderConfig({
285
+ fetchImpl: async (input, init) => {
286
+ request = { url: String(input), init };
287
+ return jsonResponse({ data: [{ id: " glm5.3 " }, { id: "glm5.3" }, { id: "unknown-chat" }, { id: "embedding" }, { id: "image" }, { id: "speech" }, { id: "rerank" }] });
288
+ },
289
+ });
290
+ const fallbackId = "deepseek-v4-flash";
291
+ assert.ok(fallbackId);
292
+
293
+ const models = await config.refreshModels?.(refreshContext({ type: "api_key", key: "test-secret" }));
294
+
295
+ assert.equal(request?.url, `${NAN_PROVIDER_BASE_URL}/models`);
296
+ assert.equal(request?.init?.method, "GET");
297
+ assert.equal(new Headers(request?.init?.headers).get("authorization"), "Bearer test-secret");
298
+ assert.equal(request?.init?.redirect, "error");
299
+ assert.deepEqual(models?.map((model) => model.id), ["glm5.3"]);
300
+ assert.ok(!models?.some((model) => model.id === fallbackId));
301
+
302
+ const known = models?.[0];
303
+ assert.equal(known?.api, "openai-completions");
304
+ assert.equal(known?.reasoning, true);
305
+ assert.deepEqual(known?.input, ["text", "image"]);
306
+ assert.equal(known?.contextWindow, 1_000_000);
307
+ assert.equal(known?.maxTokens, 32_768);
308
+ assert.deepEqual(known?.cost, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
309
+ });
310
+
311
+ test("known chat models retain documented capabilities without advertising audio", async () => {
312
+ const expected = [
313
+ ["glm5.3", 1_000_000, ["text", "image"], 32_768],
314
+ ["deepseek-v4-flash", 1_000_000, ["text", "image"], 16_384],
315
+ ["glm5.3-flash", 1_000_000, ["text", "image"], 32_768],
316
+ ["qwen3.8-flash", 1_048_576, ["text", "image"], 131_000],
317
+ ["mimo-v2.6-flash", 1_000_000, ["text", "image"], 32_768],
318
+ ["gemma4", 262_000, ["text", "image"], 65_536],
319
+ ["qwen3.6", 262_000, ["text", "image"], 65_536],
320
+ ] as const;
321
+ const config = createNanProviderConfig({
322
+ fetchImpl: async () => jsonResponse({ data: expected.map(([id]) => ({ id })) }),
323
+ });
324
+ const models = await config.refreshModels?.(refreshContext({ type: "api_key", key: "test-key" }));
325
+ assert.equal(models?.length, expected.length);
326
+ for (const [index, [id, contextWindow, input, maxTokens]] of expected.entries()) {
327
+ const model = models?.[index];
328
+ assert.equal(model?.id, id);
329
+ assert.equal(model?.reasoning, true);
330
+ assert.equal(model?.contextWindow, contextWindow);
331
+ assert.deepEqual(model?.input, input);
332
+ assert.equal(model?.maxTokens, maxTokens);
333
+ }
334
+ });
335
+
336
+ test("a successful list containing only unknown or non-chat IDs stays empty", async () => {
337
+ const config = createNanProviderConfig({
338
+ fetchImpl: async () => jsonResponse({ data: ["unknown", "embedding", "image", "speech", "rerank"].map((id) => ({ id })) }),
339
+ });
340
+ const context = refreshContext({ type: "api_key", key: "test-key" });
341
+ assert.deepEqual(await config.refreshModels?.(context), []);
342
+ assert.deepEqual(await config.refreshModels?.({ ...context, allowNetwork: false }), []);
343
+ });
344
+
345
+ test("a successful empty key-scoped catalog does not restore offline fallback models", async () => {
346
+ const config = createNanProviderConfig({ fetchImpl: async () => jsonResponse({ data: [] }) });
347
+ assert.ok(config.models && config.models.length > 0);
348
+
349
+ const models = await config.refreshModels?.(refreshContext({ type: "api_key", key: "test-secret" }));
350
+
351
+ assert.deepEqual(models, []);
352
+ assert.deepEqual(await config.refreshModels({
353
+ ...refreshContext({ type: "api_key", key: "test-secret" }), allowNetwork: false,
354
+ }), []);
355
+ });
356
+
357
+ test("failed or malformed discovery preserves the documented baseline or last successful catalog", async () => {
358
+ const failingFetches: Array<typeof fetch> = [
359
+ async () => jsonResponse({ error: "unavailable" }, 503),
360
+ async () => new Response("not-json", { status: 200 }),
361
+ async () => jsonResponse({ models: [{ id: "not-the-supported-shape" }] }),
362
+ async () => {
363
+ throw new Error("network unavailable");
364
+ },
365
+ ];
366
+
367
+ for (const fetchImpl of failingFetches) {
368
+ const config = createNanProviderConfig({ fetchImpl });
369
+ const baseline = config.models;
370
+ const afterFailure = await config.refreshModels?.(refreshContext({ type: "api_key", key: "test-secret" }));
371
+ assert.deepEqual(afterFailure, baseline);
372
+ }
373
+
374
+ let fail = false;
375
+ const config = createNanProviderConfig({
376
+ fetchImpl: async () => fail ? jsonResponse({ error: "unavailable" }, 503) : jsonResponse({ data: [{ id: "glm5.3" }] }),
377
+ });
378
+ const live = await config.refreshModels?.(refreshContext({ type: "api_key", key: "test-secret" }));
379
+ fail = true;
380
+ assert.deepEqual(await config.refreshModels?.(refreshContext({ type: "api_key", key: "test-secret" })), live);
381
+ assert.deepEqual(await config.refreshModels({
382
+ ...refreshContext({ type: "api_key", key: "test-secret" }), allowNetwork: false,
383
+ }), live);
384
+ });
385
+
386
+ test("offline model refresh does not make a network request", async () => {
387
+ let calls = 0;
388
+ const config = createNanProviderConfig({
389
+ fetchImpl: async () => {
390
+ calls++;
391
+ return jsonResponse({ data: [] });
392
+ },
393
+ });
394
+ const context = refreshContext({ type: "api_key", key: "test-secret" });
395
+ const models = await config.refreshModels?.({ ...context, allowNetwork: false });
396
+ assert.equal(calls, 0);
397
+ assert.deepEqual(models, config.models);
398
+ assert.deepEqual(models.map((model) => model.id), DOCUMENTED_CHAT_IDS);
399
+ const changed = await config.refreshModels({
400
+ ...refreshContext({ type: "api_key", key: "different-key" }), allowNetwork: false,
401
+ });
402
+ assert.deepEqual(changed.map((model) => model.id), DOCUMENTED_CHAT_IDS);
403
+ assert.equal(calls, 0);
404
+ });
405
+
406
+ test("credential changes discard previous live models before failed, offline, or cancelled discovery", async () => {
407
+ for (const mode of ["failure", "offline", "cancelled", "removed"] as const) {
408
+ let calls = 0;
409
+ const config = createNanProviderConfig({
410
+ fetchImpl: async () => ++calls === 1
411
+ ? jsonResponse({ data: [{ id: "glm5.3" }] })
412
+ : jsonResponse({ error: "denied" }, 401),
413
+ });
414
+ await config.refreshModels?.(refreshContext({ type: "api_key", key: "first-key" }));
415
+ const controller = new AbortController();
416
+ if (mode === "cancelled") controller.abort();
417
+ const context = refreshContext(mode === "removed" ? undefined : { type: "api_key", key: "second-key" });
418
+ const models = await config.refreshModels?.({
419
+ ...context,
420
+ allowNetwork: mode !== "offline",
421
+ signal: controller.signal,
422
+ });
423
+ assert.deepEqual(models, config.models, mode);
424
+ assert.equal(calls, mode === "offline" || mode === "cancelled" ? 1 : 2);
425
+ }
426
+ });
427
+
428
+ test("same-key failure retains an authoritative empty catalog", async () => {
429
+ let calls = 0;
430
+ const config = createNanProviderConfig({
431
+ fetchImpl: async () => ++calls === 1 ? jsonResponse({ data: [] }) : jsonResponse({}, 503),
432
+ });
433
+ const context = refreshContext({ type: "api_key", key: "empty-key" });
434
+ assert.deepEqual(await config.refreshModels?.(context), []);
435
+ assert.deepEqual(await config.refreshModels?.(context), []);
436
+ });
437
+
438
+ test("cancelling discovery aborts fetch and does not restore the previous key's catalog", async () => {
439
+ let calls = 0;
440
+ let requestSignal: AbortSignal | undefined;
441
+ let started!: () => void;
442
+ const pending = new Promise<void>((resolve) => { started = resolve; });
443
+ const config = createNanProviderConfig({
444
+ fetchImpl: async (_input, init) => {
445
+ if (++calls === 1) return jsonResponse({ data: [{ id: "glm5.3" }] });
446
+ requestSignal = init?.signal as AbortSignal;
447
+ return new Promise<Response>((_resolve, reject) => {
448
+ requestSignal!.addEventListener("abort", () => reject(new Error("cancelled")), { once: true });
449
+ started();
450
+ });
451
+ },
452
+ });
453
+ await config.refreshModels?.(refreshContext({ type: "api_key", key: "first-key" }));
454
+ const controller = new AbortController();
455
+ const result = config.refreshModels?.({
456
+ ...refreshContext({ type: "api_key", key: "second-key" }), signal: controller.signal,
457
+ });
458
+ await pending;
459
+ controller.abort();
460
+ assert.deepEqual(await result, config.models);
461
+ assert.equal(requestSignal?.aborted, true);
462
+ });
463
+
464
+ test("an old in-flight discovery cannot overwrite a changed credential's catalog", async () => {
465
+ let resolveOld!: (response: Response) => void;
466
+ const config = createNanProviderConfig({
467
+ fetchImpl: async (_input, init) => {
468
+ if (new Headers(init?.headers).get("authorization") === "Bearer first-key") {
469
+ return new Promise<Response>((resolve) => { resolveOld = resolve; });
470
+ }
471
+ return jsonResponse({ data: [] });
472
+ },
473
+ });
474
+ const old = config.refreshModels?.(refreshContext({ type: "api_key", key: "first-key" }));
475
+ assert.deepEqual(await config.refreshModels?.(refreshContext({ type: "api_key", key: "second-key" })), []);
476
+ resolveOld(jsonResponse({ data: [{ id: "glm5.3" }] }));
477
+ assert.deepEqual(await old, []);
478
+ assert.deepEqual(await config.refreshModels?.({
479
+ ...refreshContext({ type: "api_key", key: "second-key" }), allowNetwork: false,
480
+ }), []);
481
+ });
@@ -53,6 +53,7 @@ test("without the env var unpinned digests yield a skip with a distinct reason",
53
53
  const unpinned = requireNativeBinary({ ...PINNED, digestsPinned: false, env: {} });
54
54
 
55
55
  assert.equal(unpinned.run, false);
56
+ assert.ok("reason" in unresolved);
56
57
  assert.match(unpinned.reason as string, /digest/i);
57
58
  assert.notEqual(unpinned.reason, unresolved.reason, "the two causes must be distinguishable in output");
58
59
  });
@@ -110,6 +111,7 @@ test("without the env var a set but non-existent dev binary path yields a distin
110
111
  const missingVar = requireDevBinary({ devBinaryPath: undefined, exists: false, env: {} });
111
112
  const badPath = requireDevBinary({ devBinaryPath: "/no/such/gentle-ai", exists: false, env: {} });
112
113
  assert.equal(badPath.run, false);
114
+ assert.ok("reason" in missingVar);
113
115
  assert.match(badPath.reason as string, /\/no\/such\/gentle-ai/);
114
116
  assert.notEqual(badPath.reason, missingVar.reason, "the two causes must be distinguishable in output");
115
117
  });
@@ -367,11 +367,23 @@ test("3.7.0 explicitly repeats 3.6.1 because provider contract 1.2.0 is unchange
367
367
  assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["3.6.1"] as Record<string, boolean>);
368
368
  });
369
369
 
370
+ test("4.0.0 explicitly repeats 3.7.0 because provider contract 1.2.0 and capabilities/v2.6 are unchanged", () => {
371
+ const contract = NATIVE_CLI_CONTRACTS["4.0.0"] as Record<string, boolean>;
372
+ assert.ok(contract);
373
+ assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["3.7.0"] as Record<string, boolean>);
374
+ });
375
+
376
+ test("4.1.1 explicitly repeats 4.0.0 because capabilities/v2.6 is unchanged despite provider contract 1.3.0", () => {
377
+ const contract = NATIVE_CLI_CONTRACTS["4.1.1"] as Record<string, boolean>;
378
+ assert.ok(contract);
379
+ assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["4.0.0"] as Record<string, boolean>);
380
+ });
381
+
370
382
  test("no shipped version key was added beyond the pin bump", () => {
371
383
  // Rows are promises to consumers, so a new key only ever appears in a
372
384
  // dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
373
385
  // v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
374
386
  // so they get no row: a row asserts ground truth measured against a binary
375
387
  // Pi actually ran, and the table only has to be ascending, not gapless.
376
- assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1", "3.0.0", "3.0.1", "3.1.0", "3.2.1", "3.4.0", "3.5.0", "3.6.0", "3.6.1", "3.7.0"]);
388
+ assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1", "3.0.0", "3.0.1", "3.1.0", "3.2.1", "3.4.0", "3.5.0", "3.6.0", "3.6.1", "3.7.0", "4.0.0", "4.1.1"]);
377
389
  });