@pensar/apex 2.6.1 → 2.7.0-canary.1572d9a4

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 (328) hide show
  1. package/build/{agent-ayreaj6z.js → agent-3g1b578v.js} +6 -5
  2. package/build/{agent-yby11zaq.js → agent-p8cezgbb.js} +7 -6
  3. package/build/{agent-runs-g37vkrjr.js → agent-runs-f86emmgg.js} +23 -20
  4. package/build/agent-vaq31je6.js +14 -0
  5. package/build/agent-vvdqga09.js +14 -0
  6. package/build/{apps-xn6km9yn.js → apps-3stwnmen.js} +16 -13
  7. package/build/{auth-d105wc64.js → auth-7md9kwda.js} +16 -13
  8. package/build/blackboxAgent-ct8bejef.js +14 -0
  9. package/build/{blackboxPentest-ps78v3w7.js → blackboxPentest-w2nr4md3.js} +21 -15
  10. package/build/{cli-c05y640n.js → cli-1j28cbhy.js} +1 -1
  11. package/build/{cli-d37a42dc.js → cli-2jz2jz06.js} +206 -90
  12. package/build/{cli-az3xq70v.js → cli-4a8dsz2k.js} +4 -4
  13. package/build/{cli-qgz1q9g5.js → cli-58781vcf.js} +1192 -1473
  14. package/build/{cli-gx71jcke.js → cli-7067zzeb.js} +1 -1
  15. package/build/{cli-3sp6nd8d.js → cli-81smmd5w.js} +5 -5
  16. package/build/{cli-k2nwxta0.js → cli-8b25dhky.js} +2 -2
  17. package/build/{cli-fy6vh2a7.js → cli-8sw5zpqz.js} +1 -1
  18. package/build/{cli-709rvekb.js → cli-cg3ypx5n.js} +14 -7
  19. package/build/{cli-dpsw2fe6.js → cli-e9v6zg6h.js} +40941 -40172
  20. package/build/cli-fjxmphg1.js +36 -0
  21. package/build/{cli-7eea4e2y.js → cli-gzb6qrzh.js} +31 -60
  22. package/build/{cli-65g6rcat.js → cli-khnf5xrf.js} +2 -1
  23. package/build/{cli-1ve18npr.js → cli-mdzt2e8j.js} +4 -2
  24. package/build/{cli-n2en7j5b.js → cli-n8mmw4c4.js} +1 -1
  25. package/build/cli-pndb7mh9.js +1193 -0
  26. package/build/cli-px6ehzqx.js +54 -0
  27. package/build/{cli-xtc2t6me.js → cli-qs4msn6x.js} +111 -47
  28. package/build/{cli-kb21qhvm.js → cli-rvybdhgn.js} +4 -3
  29. package/build/{cli-ae8jqxqr.js → cli-v739h7hb.js} +2 -2
  30. package/build/{cli-ngq9dr8n.js → cli-wm8ech0x.js} +3 -3
  31. package/build/cli.js +170 -51
  32. package/build/{config-zpqkd5cz.js → config-e5v1bkh1.js} +1 -1
  33. package/build/{fastStrike-mcjdvwez.js → fastStrike-efp0rjs3.js} +74 -17
  34. package/build/{fixes-kytx5dmj.js → fixes-926cw1ej.js} +16 -13
  35. package/build/{index-n65z6v7b.js → index-3t41p258.js} +7 -6
  36. package/build/{index-qv4cc65p.js → index-bv09af6g.js} +19 -15
  37. package/build/{index-je6hcsqr.js → index-bv6dtwse.js} +1 -1
  38. package/build/{index-4vqdgb42.js → index-jt52ffgn.js} +1657 -1400
  39. package/build/{index-ns4m4s01.js → index-np405b9e.js} +11 -7
  40. package/build/{index-r9yjkfrv.js → index-qys178kp.js} +5 -4
  41. package/build/{index-wqetp6gq.js → index-tpyzq2wq.js} +42 -11
  42. package/build/{index-fv64eb32.js → index-vrcdyebh.js} +2 -2
  43. package/build/{index-dr3tg1w3.js → index-z1g8vbtm.js} +1 -1
  44. package/build/{issues-eb3q9d3a.js → issues-sncyqh14.js} +16 -13
  45. package/build/{knowledge-fqsvtx0f.js → knowledge-esgvy5ws.js} +16 -13
  46. package/build/lance-sacfgs1z.js +232 -0
  47. package/build/lib/{chunk-9yksqgzr.js → chunk-4aevegnc.js} +77 -45
  48. package/build/lib/{chunk-9tt2021m.js → chunk-4kf4a15q.js} +13 -13
  49. package/build/lib/{chunk-fy3bbbqy.js → chunk-5avnedwm.js} +7 -1
  50. package/build/lib/{chunk-bb0ekhkw.js → chunk-5nz5tk3p.js} +76 -22
  51. package/build/lib/{chunk-4hwzqthr.js → chunk-7s8e5wrd.js} +3 -2
  52. package/build/lib/{chunk-yrnrbjes.js → chunk-85kpxwzs.js} +4 -2
  53. package/build/lib/{chunk-mg03jdmt.js → chunk-9jkzk9nc.js} +1 -1
  54. package/build/lib/{chunk-r4j8ybn9.js → chunk-9wwts6r6.js} +824 -77
  55. package/build/lib/{chunk-nhfax4gb.js → chunk-bxznzxdy.js} +5 -1
  56. package/build/lib/{chunk-r2dkp4tb.js → chunk-cqxm95md.js} +1 -1
  57. package/build/lib/{chunk-w66ce82a.js → chunk-e3agqspc.js} +12 -4
  58. package/build/lib/{chunk-ba41jh41.js → chunk-eh5yw385.js} +1137 -333
  59. package/build/lib/{chunk-1azdancq.js → chunk-ewc9r84m.js} +5 -1
  60. package/build/lib/{chunk-2njv7wz3.js → chunk-hbyxyxdc.js} +4 -0
  61. package/build/lib/{chunk-e8yy2k73.js → chunk-jhhwts6w.js} +28 -2
  62. package/build/lib/{chunk-1jvwqnem.js → chunk-kbxbd1kn.js} +108 -44
  63. package/build/lib/{chunk-7pwa92rn.js → chunk-mph5fvk7.js} +1 -1
  64. package/build/lib/chunk-psc23epq.js +107 -0
  65. package/build/lib/{chunk-acgbyf5h.js → chunk-qxad7qjd.js} +4 -2
  66. package/build/lib/{chunk-3c00n0cy.js → chunk-r0v3vvgm.js} +54 -33
  67. package/build/lib/{chunk-1yct6cra.js → chunk-rt13120a.js} +1 -1
  68. package/build/lib/{chunk-v3dk9j7k.js → chunk-sgya016v.js} +93 -22
  69. package/build/lib/{chunk-1fy8azee.js → chunk-txef92hq.js} +1 -1
  70. package/build/lib/{chunk-2qsq76xq.js → chunk-vcd90qrn.js} +189 -90
  71. package/build/lib/{chunk-7gjdygcx.js → chunk-vwr9w3ab.js} +2 -0
  72. package/build/lib/{chunk-z137x08b.js → chunk-wf6ak0m3.js} +92 -70
  73. package/build/lib/{chunk-xhdpgpkm.js → chunk-xft0ms9j.js} +2 -2
  74. package/build/lib/core/agents/agentRuntime.d.ts +3 -1
  75. package/build/lib/core/agents/defineAgent.d.ts +3 -2
  76. package/build/lib/core/agents/lance.d.ts +153 -0
  77. package/build/lib/core/agents/lanceJudge.d.ts +3 -0
  78. package/build/lib/core/agents/offSecAgent/index.d.ts +3 -1
  79. package/build/lib/core/agents/offSecAgent/index.js +16 -14
  80. package/build/lib/core/agents/offSecAgent/offensiveSecurityAgent.d.ts +4 -0
  81. package/build/lib/core/agents/offSecAgent/prompt.d.ts +3 -1
  82. package/build/lib/core/agents/offSecAgent/subagentSpawner.d.ts +18 -2
  83. package/build/lib/core/agents/offSecAgent/subagentSpawner.js +10 -8
  84. package/build/lib/core/agents/offSecAgent/toolExecutionContext.d.ts +3 -0
  85. package/build/lib/core/agents/offSecAgent/toolLifecycle.d.ts +1 -0
  86. package/build/lib/core/agents/offSecAgent/tools/index.js +10 -6
  87. package/build/lib/core/agents/offSecAgent/tools/listMemories.d.ts +11 -1
  88. package/build/lib/core/agents/offSecAgent/tools/reportError.d.ts +2 -1
  89. package/build/lib/core/agents/offSecAgent/tools/runPentestWorkflow.d.ts +20 -1
  90. package/build/lib/core/agents/offSecAgent/tools/spawnPentestAgent.d.ts +4 -7
  91. package/build/lib/core/agents/offSecAgent/tools/spawnPentestSwarm.d.ts +7 -1
  92. package/build/lib/core/agents/offSecAgent/tools/types.d.ts +6 -3
  93. package/build/lib/core/agents/offSecAgent/tools/whiteboxCandidates.d.ts +2 -2
  94. package/build/lib/core/agents/offSecAgent/types.d.ts +19 -3
  95. package/build/lib/core/agents/specialized/attackSurface/blackboxAgent.js +13 -13
  96. package/build/lib/core/agents/specialized/attackSurface/schemas.js +12 -12
  97. package/build/lib/core/agents/specialized/authenticationAgent/index.js +14 -14
  98. package/build/lib/core/agents/specialized/codeAgent/agent.js +13 -13
  99. package/build/lib/core/agents/specialized/codeAgent/prompts.d.ts +1 -0
  100. package/build/lib/core/agents/specialized/cvssScorer/index.d.ts +2 -1
  101. package/build/lib/core/agents/specialized/findingJudge/agent.js +11 -11
  102. package/build/lib/core/agents/specialized/findingJudge/index.d.ts +2 -1
  103. package/build/lib/core/agents/specialized/findingJudge/index.js +4 -4
  104. package/build/lib/core/agents/specialized/patching/index.js +14 -14
  105. package/build/lib/core/agents/specialized/pentest/agent.d.ts +18 -4
  106. package/build/lib/core/agents/specialized/pentest/agent.js +15 -13
  107. package/build/lib/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.d.ts +2 -1
  108. package/build/lib/core/agents/specialized/whiteboxAttackSurface/index.js +13 -13
  109. package/build/lib/core/ai/ai.d.ts +29 -70
  110. package/build/lib/core/ai/ai.js +5 -3
  111. package/build/lib/core/ai/index.d.ts +1 -0
  112. package/build/lib/core/ai/index.js +16 -4
  113. package/build/lib/core/ai/models/claude-capabilities.d.ts +3 -2
  114. package/build/lib/core/ai/models/concentrate.d.ts +1 -0
  115. package/build/lib/core/ai/models/index.d.ts +1 -1
  116. package/build/lib/core/ai/providers/concentrate.d.ts +1 -0
  117. package/build/lib/core/ai/reasoning.d.ts +44 -0
  118. package/build/lib/core/ai/utils.d.ts +2 -0
  119. package/build/lib/core/ai/utils.js +5 -3
  120. package/build/lib/core/api/index.d.ts +6 -0
  121. package/build/lib/core/api/index.js +32 -23
  122. package/build/lib/core/api/lance.d.ts +6 -0
  123. package/build/lib/core/api/offesecAgent.d.ts +1 -0
  124. package/build/lib/core/api/reasoning.d.ts +12 -0
  125. package/build/lib/core/api/targetedPentest.d.ts +1 -0
  126. package/build/lib/core/api/targetedPentest.js +14 -14
  127. package/build/lib/core/cli/reasoning.d.ts +4 -0
  128. package/build/lib/core/config/config.d.ts +10 -2
  129. package/build/lib/core/config/customProviders.d.ts +11 -0
  130. package/build/lib/core/config/index.js +1 -1
  131. package/build/lib/core/eventBus.js +1 -1
  132. package/build/lib/core/findings/registry.d.ts +3 -1
  133. package/build/lib/core/findings/registry.js +4 -4
  134. package/build/lib/core/observability/index.js +1 -1
  135. package/build/lib/core/observability/telemetry.d.ts +4 -0
  136. package/build/lib/core/session/index.d.ts +47 -0
  137. package/build/lib/core/session/index.js +6 -4
  138. package/build/lib/core/session/persistence.d.ts +7 -0
  139. package/build/lib/core/session/persistence.js +8 -4
  140. package/build/lib/core/tools/backends/index.js +1 -1
  141. package/build/lib/core/tools/backends/resolve.d.ts +1 -0
  142. package/build/lib/core/workflows/fastStrike.d.ts +3 -1
  143. package/build/lib/core/workflows/pentest.d.ts +12 -2
  144. package/build/lib/core/workflows/pentest.js +19 -19
  145. package/build/lib/core/workflows/seams.js +5 -5
  146. package/build/lib/core/workflows/threatModel.d.ts +1 -0
  147. package/build/lib/core/workflows/whiteboxAttackSurface.d.ts +6 -4
  148. package/build/lib/core/workflows/whiteboxAttackSurface.js +15 -15
  149. package/build/lib/index.js +25 -25
  150. package/build/{logs-x62b7hk1.js → logs-bcbjpjp6.js} +16 -13
  151. package/build/{native-rollout-evidence-c9yhfqcs.js → native-rollout-evidence-rph1774m.js} +5 -5
  152. package/build/offesecAgent-81h1p6ee.js +32 -0
  153. package/build/pentest-2q20q6n1.js +23 -0
  154. package/build/{pentests-e0f32aqx.js → pentests-jgw031mc.js} +16 -13
  155. package/build/recordedRunClient-6axj6fba.js +11 -0
  156. package/build/subagentSpawner-a861gvt0.js +12 -0
  157. package/build/targetedPentest-6yxznt7s.js +91 -0
  158. package/build/{targets-xj9g3q0z.js → targets-bjah19dp.js} +16 -13
  159. package/build/threatModel-739kpexm.js +24 -0
  160. package/build/{uninstall-7s784q4a.js → uninstall-yxrn9edt.js} +1 -1
  161. package/build/{upload-4gvhwmvr.js → upload-34ewrjp5.js} +4 -4
  162. package/build/{utils-cjrz64pc.js → utils-fzm66efw.js} +5 -5
  163. package/package.json +2 -1
  164. package/src/cli/issues.test.ts +17 -1
  165. package/src/cli/lance-verifier.test.ts +57 -0
  166. package/src/cli/lance-verifier.ts +73 -0
  167. package/src/cli/lance.test.ts +71 -0
  168. package/src/cli/lance.ts +48 -0
  169. package/src/cli.ts +109 -19
  170. package/src/core/agents/agentRuntime.test.ts +42 -0
  171. package/src/core/agents/agentRuntime.ts +7 -2
  172. package/src/core/agents/defineAgent.ts +3 -1
  173. package/src/core/agents/lance.test.ts +421 -0
  174. package/src/core/agents/lance.ts +701 -0
  175. package/src/core/agents/lanceJudge.test.ts +166 -0
  176. package/src/core/agents/lanceJudge.ts +115 -0
  177. package/src/core/agents/offSecAgent/index.ts +3 -0
  178. package/src/core/agents/offSecAgent/interruptedStepFinalization.test.ts +51 -0
  179. package/src/core/agents/offSecAgent/lance.test.ts +188 -0
  180. package/src/core/agents/offSecAgent/offensiveSecurityAgent.test.ts +92 -5
  181. package/src/core/agents/offSecAgent/offensiveSecurityAgent.ts +244 -41
  182. package/src/core/agents/offSecAgent/prompt.test.ts +46 -0
  183. package/src/core/agents/offSecAgent/prompt.ts +52 -8
  184. package/src/core/agents/offSecAgent/selectiveConstruction.publicPath.test.ts +38 -0
  185. package/src/core/agents/offSecAgent/selectiveConstruction.test.ts +64 -0
  186. package/src/core/agents/offSecAgent/subagentSpawner.hooks.test.ts +11 -3
  187. package/src/core/agents/offSecAgent/subagentSpawner.reasoning.test.ts +167 -0
  188. package/src/core/agents/offSecAgent/subagentSpawner.ts +82 -28
  189. package/src/core/agents/offSecAgent/toolExecutionContext.test.ts +230 -0
  190. package/src/core/agents/offSecAgent/toolExecutionContext.ts +56 -0
  191. package/src/core/agents/offSecAgent/toolLifecycle.ts +2 -0
  192. package/src/core/agents/offSecAgent/tools/delegateAuth.ts +4 -2
  193. package/src/core/agents/offSecAgent/tools/documentFinding.test.ts +9 -4
  194. package/src/core/agents/offSecAgent/tools/documentFinding.ts +10 -9
  195. package/src/core/agents/offSecAgent/tools/listMemories.test.ts +230 -0
  196. package/src/core/agents/offSecAgent/tools/listMemories.ts +73 -6
  197. package/src/core/agents/offSecAgent/tools/reportError.test.ts +6 -0
  198. package/src/core/agents/offSecAgent/tools/reportError.ts +10 -2
  199. package/src/core/agents/offSecAgent/tools/runAttackSurface.ts +7 -4
  200. package/src/core/agents/offSecAgent/tools/runPentestWorkflow.test.ts +57 -0
  201. package/src/core/agents/offSecAgent/tools/runPentestWorkflow.ts +21 -1
  202. package/src/core/agents/offSecAgent/tools/runWhiteboxScan.ts +5 -2
  203. package/src/core/agents/offSecAgent/tools/spawnCodingAgent.ts +50 -67
  204. package/src/core/agents/offSecAgent/tools/spawnPentestAgent.test.ts +79 -3
  205. package/src/core/agents/offSecAgent/tools/spawnPentestAgent.ts +36 -16
  206. package/src/core/agents/offSecAgent/tools/spawnPentestSwarm.ts +28 -5
  207. package/src/core/agents/offSecAgent/tools/threatModelGenerator.ts +4 -3
  208. package/src/core/agents/offSecAgent/tools/types.ts +12 -2
  209. package/src/core/agents/offSecAgent/tools/updateTask.ts +5 -1
  210. package/src/core/agents/offSecAgent/tools/whiteboxCandidates.ts +11 -4
  211. package/src/core/agents/offSecAgent/types.ts +22 -1
  212. package/src/core/agents/specialized/codeAgent/agent.test.ts +7 -0
  213. package/src/core/agents/specialized/codeAgent/agent.ts +9 -2
  214. package/src/core/agents/specialized/codeAgent/prompts.ts +6 -0
  215. package/src/core/agents/specialized/cvssScorer/index.test.ts +2 -1
  216. package/src/core/agents/specialized/cvssScorer/index.ts +3 -0
  217. package/src/core/agents/specialized/findingJudge/index.ts +6 -0
  218. package/src/core/agents/specialized/pentest/agent.test.ts +155 -0
  219. package/src/core/agents/specialized/pentest/agent.ts +181 -52
  220. package/src/core/agents/specialized/pentest/results.test.ts +59 -4
  221. package/src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.concurrency.test.ts +59 -2
  222. package/src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.hooks.test.ts +127 -0
  223. package/src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.ts +74 -68
  224. package/src/core/ai/ai.durability-hooks.test.ts +25 -7
  225. package/src/core/ai/ai.lance.test.ts +581 -0
  226. package/src/core/ai/ai.operator-context.bench.test.ts +1390 -0
  227. package/src/core/ai/ai.recorded-context.test.ts +89 -8
  228. package/src/core/ai/ai.test.ts +81 -59
  229. package/src/core/ai/ai.ts +453 -327
  230. package/src/core/ai/compactionTelemetry.test.ts +69 -17
  231. package/src/core/ai/index.ts +11 -0
  232. package/src/core/ai/models/anthropic.ts +6 -0
  233. package/src/core/ai/models/bedrock.ts +18 -0
  234. package/src/core/ai/models/claude-capabilities.ts +29 -8
  235. package/src/core/ai/models/concentrate.ts +52 -0
  236. package/src/core/ai/models/index.ts +25 -4
  237. package/src/core/ai/models/models.test.ts +179 -7
  238. package/src/core/ai/models/openrouter-reasoning.json +219 -0
  239. package/src/core/ai/models/openrouter.ts +43 -1
  240. package/src/core/ai/providers/anthropic-models.test.ts +60 -1
  241. package/src/core/ai/providers/bedrock-models.test.ts +81 -0
  242. package/src/core/ai/providers/concentrate.test.ts +31 -0
  243. package/src/core/ai/providers/concentrate.ts +11 -0
  244. package/src/core/ai/providers/custom.test.ts +74 -0
  245. package/src/core/ai/providers/custom.ts +3 -1
  246. package/src/core/ai/providers/gateway-models.test.ts +16 -6
  247. package/src/core/ai/providers/pensarFormatters.ts +8 -1
  248. package/src/core/ai/reasoning.test.ts +313 -0
  249. package/src/core/ai/reasoning.ts +500 -0
  250. package/src/core/ai/streamIdleTimeout.test.ts +21 -0
  251. package/src/core/ai/utils.native-rollout-evidence.test.ts +21 -8
  252. package/src/core/ai/utils.ts +269 -163
  253. package/src/core/api/attackSurface.ts +1 -0
  254. package/src/core/api/blackboxPentest.ts +9 -1
  255. package/src/core/api/index.ts +17 -0
  256. package/src/core/api/lance.ts +23 -0
  257. package/src/core/api/offesecAgent.test.ts +51 -0
  258. package/src/core/api/offesecAgent.ts +15 -1
  259. package/src/core/api/reasoning.test.ts +213 -0
  260. package/src/core/api/reasoning.ts +44 -0
  261. package/src/core/api/recordedRun.loop.test.ts +5 -1
  262. package/src/core/api/targetedPentest.test.ts +14 -2
  263. package/src/core/api/targetedPentest.ts +52 -5
  264. package/src/core/cli/reasoning.test.ts +149 -0
  265. package/src/core/cli/reasoning.ts +95 -0
  266. package/src/core/config/config.test.ts +35 -0
  267. package/src/core/config/config.ts +11 -0
  268. package/src/core/config/customProviders.ts +21 -2
  269. package/src/core/eventBus.ts +1 -0
  270. package/src/core/findings/registry.test.ts +9 -1
  271. package/src/core/findings/registry.ts +6 -0
  272. package/src/core/observability/telemetry.test.ts +131 -79
  273. package/src/core/observability/telemetry.ts +5 -0
  274. package/src/core/runtime/sqliteRunObservation.test.ts +1 -1
  275. package/src/core/session/index.ts +38 -0
  276. package/src/core/session/persistence.test.ts +62 -0
  277. package/src/core/session/persistence.ts +56 -0
  278. package/src/core/tools/backends/resolve.ts +7 -0
  279. package/src/core/tools/backends/whiteboxJobs.test.ts +9 -8
  280. package/src/core/workflows/fastStrike.seams.test.ts +149 -0
  281. package/src/core/workflows/fastStrike.test.ts +18 -1
  282. package/src/core/workflows/fastStrike.ts +106 -11
  283. package/src/core/workflows/pentest.test.ts +104 -0
  284. package/src/core/workflows/pentest.ts +128 -7
  285. package/src/core/workflows/seams.test.ts +13 -0
  286. package/src/core/workflows/seams.ts +5 -1
  287. package/src/core/workflows/threatModel.ts +2 -0
  288. package/src/core/workflows/whiteboxAttackSurface.test.ts +74 -2
  289. package/src/core/workflows/whiteboxAttackSurface.ts +102 -73
  290. package/src/tui/command-registry.test.ts +12 -0
  291. package/src/tui/command-registry.ts +10 -0
  292. package/src/tui/components/chat/config-view.tsx +6 -10
  293. package/src/tui/components/chat/home-view.tsx +2 -2
  294. package/src/tui/components/chat/input-area.tsx +2 -2
  295. package/src/tui/components/chat/loading-indicator.tsx +20 -4
  296. package/src/tui/components/chat/message-list.tsx +1 -0
  297. package/src/tui/components/commands/__tests__/advanced-settings-lifecycle.tsx +164 -0
  298. package/src/tui/components/commands/advanced-settings.test.ts +34 -0
  299. package/src/tui/components/commands/advanced-settings.tsx +220 -31
  300. package/src/tui/components/commands/models-display.tsx +7 -1
  301. package/src/tui/components/model-picker/ModelPicker.tsx +65 -103
  302. package/src/tui/components/model-picker/ModelPickerDialog.tsx +8 -14
  303. package/src/tui/components/model-picker/__tests__/reasoning-control.tsx +162 -0
  304. package/src/tui/components/model-picker/reasoning-control.test.ts +15 -0
  305. package/src/tui/components/operator-dashboard/index.tsx +166 -50
  306. package/src/tui/components/operator-dashboard/logic.test.ts +144 -2
  307. package/src/tui/components/operator-dashboard/logic.ts +54 -3
  308. package/src/tui/components/operator-dashboard/run-events.test.ts +3 -1
  309. package/src/tui/components/shared/__tests__/tool-renderer-lifecycle.tsx +18 -0
  310. package/src/tui/components/shared/action-label.test.ts +9 -0
  311. package/src/tui/components/shared/compacting-icon.tsx +18 -0
  312. package/src/tui/components/shared/index.ts +1 -0
  313. package/src/tui/components/shared/tool-registry.ts +1 -0
  314. package/src/tui/components/shared/tool-renderer.tsx +21 -7
  315. package/src/tui/context/agent.tsx +32 -79
  316. package/src/tui/context/route.tsx +2 -0
  317. package/src/tui/index.tsx +1 -0
  318. package/src/tui/keybindings/registry.ts +2 -0
  319. package/build/agent-6w7egcf1.js +0 -13
  320. package/build/agent-9zx75feh.js +0 -13
  321. package/build/blackboxAgent-s56sgx9b.js +0 -13
  322. package/build/cli-g06s7edq.js +0 -11
  323. package/build/lib/chunk-z96pbjhd.js +0 -56
  324. package/build/offesecAgent-5whnpeta.js +0 -22
  325. package/build/pentest-e61hc38s.js +0 -22
  326. package/build/recordedRunClient-dp032c32.js +0 -11
  327. package/build/targetedPentest-rcbs4nvp.js +0 -57
  328. package/build/threatModel-jkxfa40s.js +0 -21
@@ -1,5 +1,6 @@
1
1
  import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
2
2
  import type { CreateAgentInput } from "../agents/offSecAgent";
3
+ import { sessions } from "../session";
3
4
  import {
4
5
  createOffensiveSecurityAgentClient,
5
6
  type OffensiveSecurityAgentClientInput,
@@ -80,6 +81,7 @@ function existingSessionInput(overrides: Record<string, unknown> = {}) {
80
81
  }
81
82
 
82
83
  beforeEach(() => {
84
+ vi.restoreAllMocks();
83
85
  Fake.order.length = 0;
84
86
  Fake.instances.length = 0;
85
87
  Fake.createGate = null;
@@ -88,6 +90,55 @@ beforeEach(() => {
88
90
  });
89
91
 
90
92
  describe("runOffensiveSecurityAgent", () => {
93
+ it("restores the session's effort only for the matching model", async () => {
94
+ const session = {
95
+ id: "ses_existing",
96
+ config: { modelReasoning: { model: "gpt-6.1-sol", setting: "low" } },
97
+ };
98
+ await runOffensiveSecurityAgent(
99
+ existingSessionInput({ model: "gpt-6.1-sol", session }) as never,
100
+ );
101
+ expect(Fake.instances[0].input.reasoning).toBe("low");
102
+ await runOffensiveSecurityAgent(
103
+ existingSessionInput({ model: "gemini-3-pro-preview", session }) as never,
104
+ );
105
+ expect(Fake.instances[1].input.reasoning).toBeUndefined();
106
+ });
107
+
108
+ it("persists an explicit override before starting the resumed agent", async () => {
109
+ const session = {
110
+ id: "ses_existing",
111
+ config: { modelReasoning: { model: "gpt-6.1-sol", setting: "low" } },
112
+ };
113
+ const update = vi
114
+ .spyOn(sessions, "updateModelReasoning")
115
+ .mockResolvedValue({
116
+ ...session,
117
+ config: { modelReasoning: { model: "gpt-6.1-sol", setting: "high" } },
118
+ } as never);
119
+ await runOffensiveSecurityAgent(
120
+ existingSessionInput({
121
+ model: "gpt-6.1-sol",
122
+ session,
123
+ reasoning: "high",
124
+ }) as never,
125
+ );
126
+ expect(update).toHaveBeenCalledWith("ses_existing", "gpt-6.1-sol", "high");
127
+ expect(Fake.instances[0].input.reasoning).toBe("high");
128
+ });
129
+
130
+ it("rejects unsupported explicit effort before constructing an agent", async () => {
131
+ await expect(
132
+ runOffensiveSecurityAgent(
133
+ existingSessionInput({
134
+ model: "gemini-3-pro-preview",
135
+ reasoning: "max",
136
+ }) as never,
137
+ ),
138
+ ).rejects.toThrow("not supported");
139
+ expect(Fake.instances).toHaveLength(0);
140
+ });
141
+
91
142
  it("constructs synchronously for a pre-existing session and fires onSessionReady before consume", async () => {
92
143
  const onSessionReady = vi.fn(() => Fake.order.push("onSessionReady"));
93
144
  const running = runOffensiveSecurityAgent(
@@ -6,10 +6,15 @@ import {
6
6
  } from "../agents/offSecAgent";
7
7
  import { startAgentExecution } from "../runtime/agentExecution";
8
8
  import type { SessionInfo } from "../session";
9
+ import {
10
+ persistRootReasoningOverride,
11
+ resolveRootReasoning,
12
+ } from "./reasoning";
9
13
 
10
14
  export interface RunAgentResult {
11
15
  streamResult: StreamTextResult<ToolSet, never>;
12
16
  session: SessionInfo;
17
+ lance?: import("../agents/lance").LanceMetrics;
13
18
  }
14
19
 
15
20
  /**
@@ -27,6 +32,9 @@ export async function runOffensiveSecurityAgent(
27
32
  onSessionReady?: (session: SessionInfo) => void;
28
33
  },
29
34
  ): Promise<RunAgentResult> {
35
+ const resolved = resolveRootReasoning(input);
36
+ const persisted = persistRootReasoningOverride(input);
37
+ input = persisted ? { ...resolved, session: await persisted } : resolved;
30
38
  const agent = input.session
31
39
  ? new OffensiveSecurityAgent(input as OffensiveSecurityAgentInput)
32
40
  : await OffensiveSecurityAgent.create(input as CreateAgentInput);
@@ -34,7 +42,13 @@ export async function runOffensiveSecurityAgent(
34
42
  input.onSessionReady?.(agent.session);
35
43
 
36
44
  await startAgentExecution(agent).result;
37
- return { streamResult: agent.streamResult, session: agent.session };
45
+ return {
46
+ streamResult: agent.streamResult,
47
+ session: agent.session,
48
+ ...(agent.lanceRuntime
49
+ ? { lance: agent.lanceRuntime.metrics(input.model) }
50
+ : {}),
51
+ };
38
52
  }
39
53
 
40
54
  /** Either agent-input shape, minus the abort signal the client owns. */
@@ -0,0 +1,213 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { ReasoningSetting } from "../ai";
3
+ import type { SessionInfo } from "../session";
4
+ import { runOffensiveSecurityAgent } from "./offesecAgent";
5
+ import { startTargetedPentestAgent } from "./targetedPentest";
6
+
7
+ const mocks = vi.hoisted(() => {
8
+ const inputs: Array<Record<string, unknown>> = [];
9
+ const consume = vi.fn(async () => ({ findings: [] }));
10
+ const update = vi.fn();
11
+ const abortAndDrain = vi.fn(async () => {});
12
+ class Agent {
13
+ session: unknown;
14
+ streamResult = {};
15
+ eventBus = { on: vi.fn() };
16
+ drained = Promise.resolve();
17
+ consume = consume;
18
+ abortAndDrain = abortAndDrain;
19
+ constructor(input: Record<string, unknown>) {
20
+ inputs.push(input);
21
+ this.session = input.session;
22
+ }
23
+ }
24
+ return { inputs, consume, update, abortAndDrain, Agent };
25
+ });
26
+ vi.mock("../agents/offSecAgent", () => ({
27
+ OffensiveSecurityAgent: mocks.Agent,
28
+ }));
29
+ vi.mock("../agents/specialized/pentest/agent", () => ({
30
+ TargetedPentestAgent: mocks.Agent,
31
+ }));
32
+ vi.mock("../session", () => ({
33
+ sessions: { updateModelReasoning: mocks.update },
34
+ }));
35
+
36
+ function session(model = "gpt-6.1-sol", setting: ReasoningSetting = "low") {
37
+ return {
38
+ id: "root",
39
+ config: { modelReasoning: { model, setting } },
40
+ } as SessionInfo;
41
+ }
42
+ beforeEach(() => {
43
+ mocks.inputs.length = 0;
44
+ mocks.consume.mockClear();
45
+ mocks.abortAndDrain.mockClear();
46
+ mocks.update
47
+ .mockReset()
48
+ .mockImplementation(async (_id, model, setting) => session(model, setting));
49
+ });
50
+
51
+ describe.each([
52
+ {
53
+ name: "generic",
54
+ run: (input: Record<string, unknown>) =>
55
+ runOffensiveSecurityAgent(input as never),
56
+ },
57
+ {
58
+ name: "targeted",
59
+ run: (input: Record<string, unknown>) =>
60
+ startTargetedPentestAgent(input as never).result,
61
+ },
62
+ ])("$name root reasoning", ({ run }) => {
63
+ it.each([
64
+ {
65
+ name: "matching saved selection",
66
+ overrides: {},
67
+ expected: "low",
68
+ persists: false,
69
+ },
70
+ {
71
+ name: "explicit override",
72
+ overrides: { reasoning: "high" },
73
+ expected: "high",
74
+ persists: true,
75
+ },
76
+ {
77
+ name: "explicit default",
78
+ overrides: { reasoning: "default" },
79
+ expected: "default",
80
+ persists: true,
81
+ },
82
+ {
83
+ name: "saved ahead of legacy",
84
+ overrides: { openAIReasoningEffort: "high" },
85
+ expected: "low",
86
+ persists: false,
87
+ },
88
+ {
89
+ name: "model switch",
90
+ overrides: { model: "gpt-5" },
91
+ expected: "medium",
92
+ persists: false,
93
+ },
94
+ {
95
+ name: "legacy fallback on model switch",
96
+ overrides: {
97
+ model: "concentrate:glm-5.3",
98
+ openAIReasoningEffort: "xhigh",
99
+ },
100
+ expected: "max",
101
+ persists: false,
102
+ },
103
+ {
104
+ name: "explicit none",
105
+ overrides: { model: "gpt-5.1", reasoning: "none" },
106
+ expected: "none",
107
+ persists: true,
108
+ },
109
+ {
110
+ name: "explicit disabled",
111
+ overrides: { model: "claude-sonnet-4-5", reasoning: "disabled" },
112
+ expected: "disabled",
113
+ persists: true,
114
+ },
115
+ {
116
+ name: "child override",
117
+ overrides: { subagentId: "child", reasoning: "high" },
118
+ expected: "high",
119
+ persists: false,
120
+ },
121
+ {
122
+ name: "child with no inherited choice",
123
+ overrides: { subagentId: "child" },
124
+ expected: "medium",
125
+ persists: false,
126
+ },
127
+ ])("resolves $name before constructing the agent", async ({
128
+ overrides,
129
+ expected,
130
+ persists,
131
+ }) => {
132
+ const saved = session();
133
+ const input = {
134
+ session: saved,
135
+ model: "gpt-6.1-sol",
136
+ eventBus: {},
137
+ ...overrides,
138
+ };
139
+ await run(input);
140
+ expect(mocks.inputs).toHaveLength(1);
141
+ expect(mocks.inputs[0]).toMatchObject({
142
+ model: input.model,
143
+ reasoning: expected,
144
+ });
145
+ expect(mocks.update).toHaveBeenCalledTimes(persists ? 1 : 0);
146
+ if (persists) {
147
+ expect(mocks.update).toHaveBeenCalledWith("root", input.model, expected);
148
+ expect(
149
+ (mocks.inputs[0].session as SessionInfo).config?.modelReasoning,
150
+ ).toEqual({ model: input.model, setting: expected });
151
+ }
152
+ expect(saved.config?.modelReasoning).toEqual({
153
+ model: "gpt-6.1-sol",
154
+ setting: "low",
155
+ });
156
+ });
157
+
158
+ it("rejects unsupported explicit settings before construction or persistence", async () => {
159
+ await expect(async () =>
160
+ run({ session: session(), model: "gpt-6.1-sol", reasoning: "none" }),
161
+ ).rejects.toThrow("not supported");
162
+ expect(mocks.inputs).toHaveLength(0);
163
+ expect(mocks.update).not.toHaveBeenCalled();
164
+ });
165
+ });
166
+
167
+ describe("targeted reasoning persistence lifecycle", () => {
168
+ function pendingSave() {
169
+ let resolve!: (session: SessionInfo) => void;
170
+ mocks.update.mockReturnValue(
171
+ new Promise<SessionInfo>((done) => {
172
+ resolve = done;
173
+ }),
174
+ );
175
+ const handle = startTargetedPentestAgent({
176
+ session: session(),
177
+ model: "gpt-6.1-sol",
178
+ reasoning: "high",
179
+ eventBus: {},
180
+ } as never);
181
+ return { handle, resolve };
182
+ }
183
+ it("returns a handle immediately and waits for persistence before constructing", async () => {
184
+ const { handle, resolve } = pendingSave();
185
+ expect(mocks.inputs).toHaveLength(0);
186
+ resolve(session("gpt-6.1-sol", "high"));
187
+ await handle.result;
188
+ expect(mocks.consume).toHaveBeenCalledOnce();
189
+ await handle.abortAndDrain();
190
+ expect(mocks.abortAndDrain).toHaveBeenCalledOnce();
191
+ await handle.drained;
192
+ });
193
+ it("does not start a late agent after teardown while persistence is pending", async () => {
194
+ const { handle, resolve } = pendingSave();
195
+ await handle.abortAndDrain();
196
+ resolve(session("gpt-6.1-sol", "high"));
197
+ await expect(handle.result).rejects.toMatchObject({ name: "AbortError" });
198
+ await handle.drained;
199
+ expect(mocks.inputs).toHaveLength(0);
200
+ expect(mocks.consume).not.toHaveBeenCalled();
201
+ });
202
+ it("propagates persistence failure without starting the agent", async () => {
203
+ mocks.update.mockRejectedValue(new Error("save failed"));
204
+ const handle = startTargetedPentestAgent({
205
+ session: session(),
206
+ model: "gpt-6.1-sol",
207
+ reasoning: "high",
208
+ } as never);
209
+ await expect(handle.result).rejects.toThrow("save failed");
210
+ await handle.drained;
211
+ expect(mocks.inputs).toHaveLength(0);
212
+ });
213
+ });
@@ -0,0 +1,44 @@
1
+ import { getModelSettings } from "../ai";
2
+ import type { ReasoningInput } from "../ai/reasoning";
3
+ import type { AIAuthConfig } from "../ai/utils";
4
+ import { type SessionInfo, sessions } from "../session";
5
+
6
+ type AgentReasoningInput = ReasoningInput & {
7
+ model: string;
8
+ authConfig?: AIAuthConfig;
9
+ session?: SessionInfo;
10
+ subagentId?: string;
11
+ };
12
+
13
+ export function resolveRootReasoning<T extends AgentReasoningInput>(
14
+ input: T,
15
+ ): T {
16
+ const saved = input.session?.config?.modelReasoning;
17
+ const reasoning = getModelSettings({
18
+ ...input,
19
+ reasoning:
20
+ input.reasoning ??
21
+ (!input.subagentId && saved?.model === input.model
22
+ ? saved.setting
23
+ : undefined),
24
+ }).reasoning;
25
+ return reasoning === input.reasoning ? input : { ...input, reasoning };
26
+ }
27
+
28
+ export function persistRootReasoningOverride(
29
+ input: AgentReasoningInput,
30
+ ): Promise<SessionInfo> | undefined {
31
+ const saved = input.session?.config?.modelReasoning;
32
+ if (
33
+ input.session &&
34
+ !input.subagentId &&
35
+ input.reasoning !== undefined &&
36
+ (saved?.model !== input.model || saved.setting !== input.reasoning)
37
+ ) {
38
+ return sessions.updateModelReasoning(
39
+ input.session.id,
40
+ input.model,
41
+ input.reasoning,
42
+ );
43
+ }
44
+ }
@@ -173,7 +173,11 @@ describe("recorded runs through the real agent and SDK loop", () => {
173
173
  void running.catch(() => {});
174
174
 
175
175
  try {
176
- await vi.waitFor(() => expect(hits).toEqual(["/first"]));
176
+ // Booting the agent and reaching the first tool call exceeds waitFor's 1s
177
+ // default on a loaded machine; the pause assertions below are the contract.
178
+ await vi.waitFor(() => expect(hits).toEqual(["/first"]), {
179
+ timeout: 15_000,
180
+ });
177
181
  const client = await openSqliteRunStore(join(root, "runs.sqlite"));
178
182
  try {
179
183
  const control = await client.getControl(input.runId);
@@ -49,6 +49,7 @@ import {
49
49
  // Mock ignores everything except __testResult; cast so the real input shape isn't required.
50
50
  function run(testResult: unknown) {
51
51
  return runTargetedPentestAgent({
52
+ model: "test-model",
52
53
  __testResult: testResult,
53
54
  eventBus: {},
54
55
  } as never);
@@ -104,6 +105,7 @@ describe("runTargetedPentestAgent result-shape resilience", () => {
104
105
  describe("startTargetedPentestAgent handle", () => {
105
106
  it("exposes result + abortAndDrain before consume settles", async () => {
106
107
  const handle = startTargetedPentestAgent({
108
+ model: "test-model",
107
109
  __testResult: { findings: [] },
108
110
  eventBus: {},
109
111
  } as never);
@@ -115,6 +117,7 @@ describe("startTargetedPentestAgent handle", () => {
115
117
  it("exposes the post-consume drain promise, not the pre-run placeholder", () => {
116
118
  const gate = Promise.resolve();
117
119
  const handle = startTargetedPentestAgent({
120
+ model: "test-model",
118
121
  __testResult: { findings: [] },
119
122
  __drainGate: gate,
120
123
  eventBus: {},
@@ -123,13 +126,20 @@ describe("startTargetedPentestAgent handle", () => {
123
126
  });
124
127
 
125
128
  it("passes the caller's input object to the agent untouched", () => {
126
- const input = { __testResult: { findings: [] }, eventBus: {} };
129
+ const input = {
130
+ model: "test-model",
131
+ __testResult: { findings: [] },
132
+ eventBus: {},
133
+ };
127
134
  startTargetedPentestAgent(input as never);
128
135
  expect(constructions[0]?.input).toBe(input);
129
136
  });
130
137
 
131
138
  it("wires default logging listeners only when no eventBus is provided", () => {
132
- startTargetedPentestAgent({ __testResult: { findings: [] } } as never);
139
+ startTargetedPentestAgent({
140
+ model: "test-model",
141
+ __testResult: { findings: [] },
142
+ } as never);
133
143
  expect(constructions[0]?.on).toEqual([
134
144
  "text-delta",
135
145
  "tool-call-complete",
@@ -140,6 +150,7 @@ describe("startTargetedPentestAgent handle", () => {
140
150
 
141
151
  it("leaves the agent bus untouched when the input provides an eventBus", () => {
142
152
  startTargetedPentestAgent({
153
+ model: "test-model",
143
154
  __testResult: { findings: [] },
144
155
  eventBus: {},
145
156
  } as never);
@@ -154,6 +165,7 @@ describe("runTargetedPentestAgent completion boundary", () => {
154
165
  settleDrain = resolve;
155
166
  });
156
167
  const runPromise = runTargetedPentestAgent({
168
+ model: "test-model",
157
169
  __testResult: { findings: [] },
158
170
  __drainGate: drainGate,
159
171
  eventBus: {},
@@ -7,9 +7,14 @@ import {
7
7
  type AgentExecutionHandle,
8
8
  startAgentExecution,
9
9
  } from "../runtime/agentExecution";
10
+ import {
11
+ persistRootReasoningOverride,
12
+ resolveRootReasoning,
13
+ } from "./reasoning";
10
14
 
11
15
  export type TargetedPentestResult = {
12
16
  findings: PentestResult["findings"];
17
+ reportedFindings?: string[];
13
18
  findingsPath?: string;
14
19
  pocsPath?: string;
15
20
  objectiveResults?: PentestResult["objectiveResults"];
@@ -20,7 +25,10 @@ export type TargetedPentestResult = {
20
25
  export type TargetedPentestHandle = AgentExecutionHandle<TargetedPentestResult>;
21
26
 
22
27
  /** Guard against malformed consume() shapes (don't crash on findings.length). */
23
- function normalizeResult(result: unknown): TargetedPentestResult {
28
+ function normalizeResult(
29
+ result: unknown,
30
+ findingPolicy?: PentestAgentInput["findingPolicy"],
31
+ ): TargetedPentestResult {
24
32
  const r = result as Partial<PentestResult> | null | undefined;
25
33
  if (!r || !Array.isArray(r.findings)) {
26
34
  console.error(
@@ -30,12 +38,22 @@ function normalizeResult(result: unknown): TargetedPentestResult {
30
38
  }
31
39
 
32
40
  const findings = Array.isArray(r?.findings) ? r.findings : [];
33
- console.log(`\nFound ${findings.length} vulnerabilities`);
34
- console.log(`Findings: ${r?.findingsPath}`);
35
- console.log(`POCs: ${r?.pocsPath}`);
41
+ const reportedFindings = Array.isArray(r?.reportedFindings)
42
+ ? r.reportedFindings
43
+ : [];
44
+ if (findingPolicy === "external") {
45
+ console.log(
46
+ `\nReported ${reportedFindings.length} finding(s) for external evaluation`,
47
+ );
48
+ } else {
49
+ console.log(`\nFound ${findings.length} vulnerabilities`);
50
+ console.log(`Findings: ${r?.findingsPath}`);
51
+ console.log(`POCs: ${r?.pocsPath}`);
52
+ }
36
53
 
37
54
  return {
38
55
  findings,
56
+ ...(findingPolicy === "external" ? { reportedFindings } : {}),
39
57
  findingsPath: r?.findingsPath,
40
58
  pocsPath: r?.pocsPath,
41
59
  objectiveResults: r?.objectiveResults,
@@ -47,6 +65,35 @@ function normalizeResult(result: unknown): TargetedPentestResult {
47
65
  export function startTargetedPentestAgent(
48
66
  input: PentestAgentInput,
49
67
  ): TargetedPentestHandle {
68
+ const resolved = resolveRootReasoning(input);
69
+ const persisted = persistRootReasoningOverride(input);
70
+ input = resolved;
71
+ if (!persisted) return startResolvedAgent(input);
72
+
73
+ let execution: TargetedPentestHandle | undefined;
74
+ let cancelled = false;
75
+ const started = persisted.then((session) => {
76
+ // A teardown during persistence must prevent a later agent from starting.
77
+ if (cancelled)
78
+ throw new DOMException("Agent aborted before start", "AbortError");
79
+ input.abortSignal?.throwIfAborted();
80
+ execution = startResolvedAgent({ ...input, session });
81
+ return execution;
82
+ });
83
+ return {
84
+ result: started.then((handle) => handle.result),
85
+ drained: started.then(
86
+ (handle) => handle.drained,
87
+ () => {},
88
+ ),
89
+ abortAndDrain: async () => {
90
+ cancelled = true;
91
+ await execution?.abortAndDrain();
92
+ },
93
+ };
94
+ }
95
+
96
+ function startResolvedAgent(input: PentestAgentInput): TargetedPentestHandle {
50
97
  const agent = new TargetedPentestAgent(input);
51
98
 
52
99
  if (!input.eventBus) {
@@ -62,7 +109,7 @@ export function startTargetedPentestAgent(
62
109
 
63
110
  const { result, abortAndDrain, drained } = startAgentExecution(agent);
64
111
  return {
65
- result: result.then(normalizeResult),
112
+ result: result.then((value) => normalizeResult(value, input.findingPolicy)),
66
113
  abortAndDrain,
67
114
  drained,
68
115
  };
@@ -0,0 +1,149 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildReasoningProviderOptions } from "../ai";
3
+ import type { Config } from "../config/config";
4
+ import { getSavedReasoning, resolveCliReasoning } from "./reasoning";
5
+
6
+ describe("CLI reasoning selection", () => {
7
+ it.each([
8
+ "claude-sonnet-4-5",
9
+ "claude-opus-5-5",
10
+ "anthropic/claude-opus-4.6",
11
+ "anthropic/claude-sonnet-4.5",
12
+ ])("ignores a leftover GPT preference when switching to %s", (model) => {
13
+ const reasoning = getSavedReasoning(model, {
14
+ openAIReasoningEffort: "high",
15
+ });
16
+ expect(reasoning).toBe("default");
17
+ expect(buildReasoningProviderOptions(model, { reasoning })).toEqual(
18
+ buildReasoningProviderOptions(model, {
19
+ reasoning: getSavedReasoning(model, {}),
20
+ }),
21
+ );
22
+ });
23
+ it.each([
24
+ true,
25
+ false,
26
+ ])("ignores a leftover Claude toggle (%s) when switching to GPT", (enabled) => {
27
+ const model = "gpt-6.1-sol";
28
+ expect(getSavedReasoning(model, { reasoningEnabled: enabled })).toBe(
29
+ "default",
30
+ );
31
+ expect(
32
+ getSavedReasoning(model, {
33
+ reasoningEnabled: enabled,
34
+ openAIReasoningEffort: "low",
35
+ }),
36
+ ).toBe("low");
37
+ });
38
+ it.each([
39
+ ["concentrate:glm-5.3", "none", "low"],
40
+ ["concentrate:glm-5.3", "minimal", "max"],
41
+ ["concentrate:glm-5.3", "low", "low"],
42
+ ["concentrate:glm-5.3", "medium", "high"],
43
+ ["concentrate:glm-5.3", "high", "high"],
44
+ ["concentrate:glm-5.3", "xhigh", "max"],
45
+ ["concentrate:glm-5.3", "max", "max"],
46
+ ["concentrate:glm-5.3", "ultra", "max"],
47
+ ["gpt-6.1-sol", "ultra", "max"],
48
+ ["gpt-5", "xhigh", "medium"],
49
+ ] as const)("migrates %s / %s to %s identically to the API", (model, legacy, expected) => {
50
+ const reasoning = getSavedReasoning(model, {
51
+ openAIReasoningEffort: legacy,
52
+ });
53
+ expect(reasoning).toBe(expected);
54
+ expect(buildReasoningProviderOptions(model, { reasoning })).toEqual(
55
+ buildReasoningProviderOptions(model, { openAIReasoningEffort: legacy }),
56
+ );
57
+ });
58
+ it.each([
59
+ "claude-sonnet-4-5",
60
+ "claude-opus-5-5",
61
+ "anthropic/claude-opus-4.6",
62
+ ])("migrates the %s thinking toggle identically to the API", (model) => {
63
+ for (const enabled of [true, false]) {
64
+ const reasoning = getSavedReasoning(model, { reasoningEnabled: enabled });
65
+ expect(buildReasoningProviderOptions(model, { reasoning })).toEqual(
66
+ buildReasoningProviderOptions(model, { enableThinking: enabled }),
67
+ );
68
+ }
69
+ });
70
+ it("keeps explicit canonical values ahead of legacy migration and rejects invalid new selections", () => {
71
+ const model = "concentrate:glm-5.3";
72
+ const config: Pick<Config, "modelReasoning" | "openAIReasoningEffort"> = {
73
+ modelReasoning: { [model]: "low" },
74
+ openAIReasoningEffort: "xhigh",
75
+ };
76
+ const reasoning = getSavedReasoning(model, config);
77
+ expect(reasoning).toBe("low");
78
+ expect(
79
+ buildReasoningProviderOptions(model, {
80
+ reasoning,
81
+ openAIReasoningEffort: "xhigh",
82
+ }),
83
+ ).toEqual(
84
+ buildReasoningProviderOptions(model, { openAIReasoningEffort: "low" }),
85
+ );
86
+ expect(() =>
87
+ resolveCliReasoning(["--effort", "xhigh"], model, config),
88
+ ).toThrow("not supported");
89
+ expect(() =>
90
+ buildReasoningProviderOptions(model, { reasoning: "xhigh" }),
91
+ ).toThrow("not supported");
92
+ });
93
+ it("gives an invocation precedence over the saved per-model preference", () => {
94
+ expect(
95
+ resolveCliReasoning(["--effort", "low"], "claude-opus-5-5", {
96
+ modelReasoning: { "claude-opus-5-5": "max" },
97
+ }),
98
+ ).toBe("low");
99
+ expect(resolveCliReasoning(["--effort=high"], "gpt-6.1-sol", {})).toBe(
100
+ "high",
101
+ );
102
+ });
103
+ it("does not carry incompatible settings between models", () => {
104
+ const config = { modelReasoning: { "gpt-6.1-sol": "max" as const } };
105
+ expect(getSavedReasoning("gemini-3-pro-preview", config)).toBe("default");
106
+ expect(getSavedReasoning("gpt-6.1-sol", config)).toBe("max");
107
+ });
108
+ it("migrates supported legacy preferences without offering the ultra alias", () => {
109
+ expect(
110
+ getSavedReasoning("gpt-6.1-sol", { openAIReasoningEffort: "ultra" }),
111
+ ).toBe("max");
112
+ expect(
113
+ getSavedReasoning("claude-sonnet-4-5", { reasoningEnabled: true }),
114
+ ).toBe("enabled");
115
+ expect(
116
+ getSavedReasoning("anthropic/claude-opus-4.6", {
117
+ reasoningEnabled: true,
118
+ }),
119
+ ).toBe("high");
120
+ expect(
121
+ getSavedReasoning("anthropic/claude-opus-4.6", {
122
+ reasoningEnabled: false,
123
+ }),
124
+ ).toBe("none");
125
+ });
126
+ it.each([
127
+ ["--effort"],
128
+ ["--effort", "--model"],
129
+ ["--effort", "ultra"],
130
+ ["--effort", "none"],
131
+ ["--effort", "low", "--extended-thinking"],
132
+ ])("rejects invalid flags before execution: %j", (...args) => {
133
+ expect(() => resolveCliReasoning(args, "gpt-6.1-sol", {})).toThrow();
134
+ });
135
+ it("uses a toggle only where the model supports it", () => {
136
+ expect(
137
+ resolveCliReasoning(["--extended-thinking"], "claude-sonnet-4-5", {}),
138
+ ).toBe("enabled");
139
+ expect(
140
+ resolveCliReasoning(["--no-extended-thinking"], "gemini-2.5-flash", {}),
141
+ ).toBe("disabled");
142
+ expect(() =>
143
+ resolveCliReasoning(["--no-extended-thinking"], "claude-opus-5-5", {}),
144
+ ).toThrow();
145
+ expect(() =>
146
+ resolveCliReasoning(["--extended-thinking"], "unknown", {}),
147
+ ).toThrow();
148
+ });
149
+ });