carthagent 0.0.0-stage → 1.0.7

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 (379) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -2
  3. package/bin/ctg.mjs +736 -0
  4. package/docs/budgets.md +72 -0
  5. package/docs/compaction.md +34 -0
  6. package/docs/context-efficiency-plan.md +24 -0
  7. package/docs/context-efficiency-review.md +27 -0
  8. package/docs/context-efficiency.md +49 -0
  9. package/docs/deepseek-jev-benchmark-plan.md +70 -0
  10. package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
  11. package/docs/deepseek-jev-benchmark-restart.md +9 -0
  12. package/docs/deepseek-jev-benchmark-results.md +77 -0
  13. package/docs/deepseek-jev-benchmark-running.md +60 -0
  14. package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
  15. package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
  16. package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
  17. package/docs/deepseek-jev-http400-diagnosis.md +45 -0
  18. package/docs/delivery-review-evidence.md +53 -0
  19. package/docs/delivery-timeout-repair-plan.md +51 -0
  20. package/docs/evaluation-readiness.md +31 -0
  21. package/docs/evaluation.md +70 -0
  22. package/docs/extending.md +354 -0
  23. package/docs/glm-pilot-execution.md +9 -0
  24. package/docs/glm-pilot-plan.md +15 -0
  25. package/docs/glm-pilot-results.md +71 -0
  26. package/docs/graph-context-optimization.md +15 -0
  27. package/docs/jev-repository-pilot-plan.md +25 -0
  28. package/docs/jev-repository-pilot-protocol.md +39 -0
  29. package/docs/jev-repository-pilot-restart.md +7 -0
  30. package/docs/jev-repository-pilot-results.md +79 -0
  31. package/docs/jev.md +32 -0
  32. package/docs/model-registry.md +25 -0
  33. package/docs/native-compatibility.md +117 -0
  34. package/docs/opencode-archive.md +9 -0
  35. package/docs/opencode-migration.md +115 -0
  36. package/docs/opencode-runtime.md +94 -0
  37. package/docs/opencode-workspace.md +31 -0
  38. package/docs/owned-runtime-workflow-registration.md +17 -0
  39. package/docs/plan-advisor-evaluation.md +38 -0
  40. package/docs/planning-startup-fix-plan.md +23 -0
  41. package/docs/planning-startup-fix-review.md +24 -0
  42. package/docs/planning.md +118 -0
  43. package/docs/production-merge-review.md +23 -0
  44. package/docs/quality.md +35 -0
  45. package/docs/repository-benchmark-design.md +111 -0
  46. package/docs/review-assurance.md +9 -0
  47. package/docs/review-experiment-results.md +59 -0
  48. package/docs/tested-planning-roadmap.md +58 -0
  49. package/docs/tested-planning.md +58 -0
  50. package/docs/tui-pickers.md +9 -0
  51. package/docs/webdriver.md +83 -0
  52. package/docs/workflow-modes.md +26 -0
  53. package/docs/workflow.d2 +17 -0
  54. package/docs/workflow.svg +149 -0
  55. package/docs/workspace-coordination.md +9 -0
  56. package/evaluate.mjs +249 -0
  57. package/extensions/codegraph.ts +36 -0
  58. package/extensions/delivery.ts +819 -0
  59. package/extensions/experiential.ts +21 -0
  60. package/extensions/subscriptions.ts +21 -0
  61. package/guidance/profiles/authenticated-web.json +24 -0
  62. package/guidance/profiles/data-visualization.json +25 -0
  63. package/guidance/profiles/database-change.json +24 -0
  64. package/guidance/profiles/external-api.json +24 -0
  65. package/guidance/profiles/transactional-data.json +25 -0
  66. package/guidance/profiles/web-application.json +26 -0
  67. package/lib/acceptance-review.mjs +4 -0
  68. package/lib/browser-bin.mjs +160 -0
  69. package/lib/browser-errors.mjs +54 -0
  70. package/lib/browser.mjs +219 -0
  71. package/lib/budget.mjs +37 -0
  72. package/lib/cdp.mjs +221 -0
  73. package/lib/cloud/client.mjs +275 -0
  74. package/lib/cloud/control-plane.mjs +350 -0
  75. package/lib/cloud/gateway.mjs +380 -0
  76. package/lib/cloud/http.mjs +135 -0
  77. package/lib/cloud/ledger.mjs +215 -0
  78. package/lib/codegraph-query.mjs +60 -0
  79. package/lib/codegraph.mjs +378 -0
  80. package/lib/context-archive.mjs +18 -0
  81. package/lib/delivery-context.mjs +132 -0
  82. package/lib/delivery-guidance.mjs +9 -0
  83. package/lib/delivery-review.mjs +94 -0
  84. package/lib/delivery.mjs +610 -0
  85. package/lib/engine.mjs +137 -0
  86. package/lib/guidance.mjs +71 -0
  87. package/lib/jev-advisor.mjs +47 -0
  88. package/lib/opencode/archive.mjs +122 -0
  89. package/lib/opencode/compatibility.mjs +19 -0
  90. package/lib/opencode/credentials.mjs +205 -0
  91. package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
  92. package/lib/opencode/evaluation.mjs +242 -0
  93. package/lib/opencode/file-lock.mjs +86 -0
  94. package/lib/opencode/headless.mjs +200 -0
  95. package/lib/opencode/review.mjs +213 -0
  96. package/lib/opencode/runtime.mjs +246 -0
  97. package/lib/opencode/service.mjs +249 -0
  98. package/lib/opencode/session-options.mjs +60 -0
  99. package/lib/opencode/workspace.mjs +133 -0
  100. package/lib/openrouter-jev.mjs +37 -0
  101. package/lib/outcome-contract.mjs +21 -0
  102. package/lib/plan-advisor.mjs +38 -0
  103. package/lib/planning-access.mjs +7 -0
  104. package/lib/planning-advice.mjs +28 -0
  105. package/lib/planning-discovery.mjs +14 -0
  106. package/lib/planning-guide.mjs +34 -0
  107. package/lib/planning-view.mjs +57 -0
  108. package/lib/planning.mjs +155 -0
  109. package/lib/probe-diagnostics.mjs +13 -0
  110. package/lib/providers/experiential.mjs +292 -0
  111. package/lib/providers/model-catalog.json +700 -0
  112. package/lib/providers/model-catalog.mjs +7 -0
  113. package/lib/providers/names.mjs +13 -0
  114. package/lib/providers/subscription-models.mjs +483 -0
  115. package/lib/reports.mjs +80 -0
  116. package/lib/review-state.mjs +49 -0
  117. package/lib/review.mjs +114 -0
  118. package/lib/scenarios.mjs +99 -0
  119. package/lib/ship-auth.mjs +44 -0
  120. package/lib/ship-jev.mjs +59 -0
  121. package/lib/ship-telemetry.mjs +183 -0
  122. package/lib/tui/ansi.mjs +255 -0
  123. package/lib/tui/app.mjs +2445 -0
  124. package/lib/tui/auth.mjs +249 -0
  125. package/lib/tui/carthage.mjs +109 -0
  126. package/lib/tui/clipboard.mjs +122 -0
  127. package/lib/tui/d2.mjs +431 -0
  128. package/lib/tui/feed.mjs +467 -0
  129. package/lib/tui/framing.mjs +80 -0
  130. package/lib/tui/glyphs.mjs +172 -0
  131. package/lib/tui/hash-worker.mjs +11 -0
  132. package/lib/tui/mock-engine.mjs +222 -0
  133. package/lib/tui/models.mjs +73 -0
  134. package/lib/tui/picker.mjs +18 -0
  135. package/lib/tui/theme.mjs +663 -0
  136. package/lib/tui/title.mjs +7 -0
  137. package/lib/update-channel.mjs +62 -0
  138. package/lib/update.mjs +230 -0
  139. package/lib/webdriver.mjs +82 -0
  140. package/lib/workflow.mjs +88 -0
  141. package/lib/workspace.mjs +39 -0
  142. package/metadata.json +8 -0
  143. package/package.json +94 -4
  144. package/plugins/carthagent/index.ts +3 -0
  145. package/plugins/carthagent/package.json +28 -0
  146. package/plugins/carthagent/src/archive-rpc.ts +46 -0
  147. package/plugins/carthagent/src/bootstrap.ts +38 -0
  148. package/plugins/carthagent/src/index.ts +62 -0
  149. package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
  150. package/plugins/carthagent/src/rpc.ts +76 -0
  151. package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
  152. package/plugins/carthagent/src/server/budgets.ts +21 -0
  153. package/plugins/carthagent/src/server/codegraph.ts +46 -0
  154. package/plugins/carthagent/src/server/delivery.ts +488 -0
  155. package/plugins/carthagent/src/server/device-auth.ts +91 -0
  156. package/plugins/carthagent/src/server/native-delivery.ts +26 -0
  157. package/plugins/carthagent/src/server/orchestration.ts +56 -0
  158. package/plugins/carthagent/src/server/policy.ts +24 -0
  159. package/plugins/carthagent/src/server/providers.ts +424 -0
  160. package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
  161. package/plugins/carthagent/src/server/session-controls.ts +82 -0
  162. package/plugins/carthagent/src/server/telemetry.ts +92 -0
  163. package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
  164. package/plugins/carthagent/src/session-rpc.ts +31 -0
  165. package/plugins/carthagent/src/testing.ts +85 -0
  166. package/plugins/carthagent/src/workspace-rpc.ts +133 -0
  167. package/plugins/carthagent/test-plugin/index.ts +4 -0
  168. package/plugins/carthagent/test-plugin/package.json +1 -0
  169. package/plugins/carthagent/tsconfig.json +19 -0
  170. package/plugins/carthagent/tui.ts +1 -0
  171. package/prompts/delivery.md +7 -0
  172. package/public/index.html +983 -0
  173. package/scenarios/cli/grade_cli.py +69 -0
  174. package/scenarios/cli/scenario.json +21 -0
  175. package/scenarios/game/baseline/minecraft.py +193 -0
  176. package/scenarios/game/context.md +1 -0
  177. package/scenarios/game/grade_minecraft_clone.py +603 -0
  178. package/scenarios/game/requirements.txt +2 -0
  179. package/scenarios/game/scenario.json +33 -0
  180. package/server.mjs +341 -0
  181. package/skills/game-development/SKILL.md +36 -0
  182. package/skills/game-development/assets/ursina_starter.py +98 -0
  183. package/skills/game-development/scripts/verify_ursina.py +80 -0
  184. package/skills/software-delivery/SKILL.md +50 -0
  185. package/skills/typescript-delivery/SKILL.md +69 -0
  186. package/tests/acceptance-review.test.mjs +13 -0
  187. package/tests/auth.test.mjs +234 -0
  188. package/tests/bounded-summary.test.mjs +68 -0
  189. package/tests/browser-bin.test.mjs +91 -0
  190. package/tests/browser.test.mjs +96 -0
  191. package/tests/budget-engine.test.mjs +110 -0
  192. package/tests/budget.test.mjs +23 -0
  193. package/tests/cli.test.mjs +245 -0
  194. package/tests/clipboard.test.mjs +79 -0
  195. package/tests/cloud-client.test.mjs +129 -0
  196. package/tests/cloud-control-plane.test.mjs +144 -0
  197. package/tests/cloud-gateway.test.mjs +147 -0
  198. package/tests/cloud-http.test.mjs +102 -0
  199. package/tests/cloud-ledger.test.mjs +75 -0
  200. package/tests/codegraph-query.test.mjs +66 -0
  201. package/tests/codegraph.test.mjs +142 -0
  202. package/tests/delivery-context.test.mjs +66 -0
  203. package/tests/delivery-review.test.mjs +139 -0
  204. package/tests/delivery.test.mjs +343 -0
  205. package/tests/engine-api.mjs +24 -0
  206. package/tests/experiential-extension.test.mjs +37 -0
  207. package/tests/experiential.test.mjs +300 -0
  208. package/tests/extension.test.mjs +921 -0
  209. package/tests/fixtures/archive-plugin/index.ts +10 -0
  210. package/tests/guidance.test.mjs +78 -0
  211. package/tests/model-registry.test.mjs +68 -0
  212. package/tests/models.test.mjs +70 -0
  213. package/tests/openai-idempotency.test.mjs +99 -0
  214. package/tests/opencode-archive-rpc.test.mjs +82 -0
  215. package/tests/opencode-delivery.test.mjs +231 -0
  216. package/tests/opencode-providers.test.mjs +414 -0
  217. package/tests/opencode-runtime.test.mjs +455 -0
  218. package/tests/opencode-session-controls.test.mjs +115 -0
  219. package/tests/opencode-session-options.test.mjs +26 -0
  220. package/tests/opencode-workspace.test.mjs +339 -0
  221. package/tests/outcome-contract.test.mjs +17 -0
  222. package/tests/picker.test.mjs +34 -0
  223. package/tests/plan-advisor.test.mjs +88 -0
  224. package/tests/plan-review-loop.test.mjs +53 -0
  225. package/tests/planning-advice.test.mjs +27 -0
  226. package/tests/planning-contract.test.mjs +65 -0
  227. package/tests/planning-startup.test.mjs +38 -0
  228. package/tests/planning-view.test.mjs +33 -0
  229. package/tests/planning.test.mjs +223 -0
  230. package/tests/probe-diagnostics.test.mjs +21 -0
  231. package/tests/pull-request-rpc.test.mjs +366 -0
  232. package/tests/review-state.test.mjs +32 -0
  233. package/tests/review.test.mjs +127 -0
  234. package/tests/server.test.mjs +252 -0
  235. package/tests/ship-jev.test.mjs +77 -0
  236. package/tests/ship-telemetry.test.mjs +333 -0
  237. package/tests/standard-workflow.test.mjs +28 -0
  238. package/tests/subscription-models.test.mjs +416 -0
  239. package/tests/subscriptions-extension.test.mjs +27 -0
  240. package/tests/terminal-title.test.mjs +13 -0
  241. package/tests/test_grade_minecraft_clone.py +199 -0
  242. package/tests/test_starter.py +78 -0
  243. package/tests/theme.test.mjs +175 -0
  244. package/tests/tui-copy-paste.test.mjs +54 -0
  245. package/tests/tui.test.mjs +808 -0
  246. package/tests/update-channels.test.mjs +129 -0
  247. package/tests/update.test.mjs +175 -0
  248. package/tests/webdriver.test.mjs +31 -0
  249. package/tests/workflow.test.mjs +47 -0
  250. package/tests/workspace-rpc.test.mjs +236 -0
  251. package/tests/workspace.test.mjs +24 -0
  252. package/tools/benchmark/advisor.test.mjs +7 -0
  253. package/tools/benchmark/advisor.ts +27 -0
  254. package/tools/benchmark/campaign.mjs +45 -0
  255. package/tools/benchmark/diagnose-request.mjs +40 -0
  256. package/tools/benchmark/eval-common.mjs +32 -0
  257. package/tools/benchmark/eval-ledger.mjs +22 -0
  258. package/tools/benchmark/eval-shop.mjs +26 -0
  259. package/tools/benchmark/eval-voxel.mjs +20 -0
  260. package/tools/benchmark/evaluate.mjs +24 -0
  261. package/tools/benchmark/evaluators.test.mjs +13 -0
  262. package/tools/benchmark/fixtures/shop/server.mjs +18 -0
  263. package/tools/benchmark/fixtures/shop/ui.html +8 -0
  264. package/tools/benchmark/freeze.mjs +30 -0
  265. package/tools/benchmark/gateway.mjs +91 -0
  266. package/tools/benchmark/gateway.test.mjs +38 -0
  267. package/tools/benchmark/generator.mjs +11 -0
  268. package/tools/benchmark/generator.test.mjs +26 -0
  269. package/tools/benchmark/guard.test.mjs +133 -0
  270. package/tools/benchmark/guard.ts +136 -0
  271. package/tools/benchmark/native-plugin/index.ts +24 -0
  272. package/tools/benchmark/planning-startup.test.mjs +47 -0
  273. package/tools/benchmark/preflight-live.mjs +31 -0
  274. package/tools/benchmark/preflight.mjs +22 -0
  275. package/tools/benchmark/replay-diagnostic.mjs +23 -0
  276. package/tools/benchmark/runner.mjs +141 -0
  277. package/tools/benchmark/runner.test.mjs +93 -0
  278. package/tools/benchmark/runtime.mjs +23 -0
  279. package/tools/benchmark/selftest.mjs +47 -0
  280. package/tools/benchmark/smoke-jev.mjs +5 -0
  281. package/tools/benchmark/tasks.mjs +20 -0
  282. package/tools/benchmark/transport.mjs +65 -0
  283. package/tools/benchmark/transport.test.mjs +44 -0
  284. package/tools/benchmark/upstream-error.mjs +18 -0
  285. package/tools/benchmark/upstream-error.test.mjs +10 -0
  286. package/tools/browser-check.mjs +109 -0
  287. package/tools/check-syntax.mjs +27 -0
  288. package/tools/evaluate-plan-advisor.mjs +20 -0
  289. package/tools/native-fixture.mjs +194 -0
  290. package/tools/native-pty-smoke.py +108 -0
  291. package/tools/native-report-plugin/index.ts +50 -0
  292. package/tools/native-report-plugin/package.json +7 -0
  293. package/tools/patch-compaction.mjs +24 -0
  294. package/tools/patch-model-catalog.mjs +12 -0
  295. package/tools/patch-print-shutdown.mjs +39 -0
  296. package/tools/repo-pilot/evaluator.cjs +32 -0
  297. package/tools/repo-pilot/reference.mjs +43 -0
  298. package/tools/repo-pilot/runner.mjs +85 -0
  299. package/tools/repo-pilot/sandbox-entry.mjs +12 -0
  300. package/tools/repo-pilot/tasks.mjs +20 -0
  301. package/tools/repo-pilot/type-evaluator.cjs +6 -0
  302. package/tools/test-native-controls.mjs +84 -0
  303. package/tools/test-native-delivery-host.mjs +307 -0
  304. package/tools/test-native-evaluation.mjs +46 -0
  305. package/tools/test-native-host.mjs +385 -0
  306. package/tools/test-native-outcomes.mjs +73 -0
  307. package/tools/test-native-package.mjs +70 -0
  308. package/tools/test-native-pty.mjs +20 -0
  309. package/tools/test-native-report.mjs +88 -0
  310. package/tools/test-native-swe-host.mjs +132 -0
  311. package/tools/test-native-swe.mjs +122 -0
  312. package/tools/test-picker-pty.py +45 -0
  313. package/tools/test-python.mjs +29 -0
  314. package/tools/test-webdriver.mjs +54 -0
  315. package/tools/verify-release-version.mjs +37 -0
  316. package/tools/webdriver-check.mjs +52 -0
  317. package/vendor/agent/bounded-summary.mjs +88 -0
  318. package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
  319. package/vendor/agent/chunks/anthropic.js +75 -0
  320. package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
  321. package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
  322. package/vendor/agent/chunks/chord-context.js +96 -0
  323. package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
  324. package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
  325. package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
  326. package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
  327. package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
  328. package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
  329. package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
  330. package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
  331. package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
  332. package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
  333. package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
  334. package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
  335. package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
  336. package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
  337. package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
  338. package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
  339. package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
  340. package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
  341. package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
  342. package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
  343. package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
  344. package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
  345. package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
  346. package/vendor/agent/chunks/github-copilot.js +2 -0
  347. package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
  348. package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
  349. package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
  350. package/vendor/agent/chunks/image-resize-worker.js +2 -0
  351. package/vendor/agent/chunks/kimi-coding.js +2 -0
  352. package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
  353. package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
  354. package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
  355. package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
  356. package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
  357. package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
  358. package/vendor/agent/chunks/openai-codex.js +75 -0
  359. package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
  360. package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
  361. package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
  362. package/vendor/agent/chunks/openrouter.js +75 -0
  363. package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
  364. package/vendor/agent/chunks/radius.js +75 -0
  365. package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
  366. package/vendor/agent/chunks/xai.js +2 -0
  367. package/vendor/agent/cli.js +3 -0
  368. package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
  369. package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
  370. package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
  371. package/vendor/agent/index.js +2 -0
  372. package/vendor/agent/modes/interactive/theme/dark.json +91 -0
  373. package/vendor/agent/modes/interactive/theme/light.json +90 -0
  374. package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
  375. package/vendor/agent/package.json +14 -0
  376. package/vendor/agent/rpc-entry.js +3 -0
  377. package/vendor/agent/theme/dark.json +91 -0
  378. package/vendor/agent/theme/light.json +90 -0
  379. package/vendor/agent/theme/theme-schema.json +356 -0
@@ -0,0 +1,13 @@
1
+ export const CARTHAGENT_SHIP_PROVIDER_ID = 'experiential-labs';
2
+ export const CARTHAGENT_SHIP_PROVIDER_NAME = 'Carthagent Ship';
3
+
4
+ export function providerDisplayName(providerId) {
5
+ return providerId === CARTHAGENT_SHIP_PROVIDER_ID ? CARTHAGENT_SHIP_PROVIDER_NAME : String(providerId || '');
6
+ }
7
+
8
+ export function modelDisplayIdentity(value) {
9
+ const identity = String(value || '');
10
+ const slash = identity.indexOf('/');
11
+ if (slash <= 0) return identity;
12
+ return `${providerDisplayName(identity.slice(0, slash))}/${identity.slice(slash + 1)}`;
13
+ }
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Subscription model discovery for the bundled engine's first-party OAuth
3
+ * providers: `anthropic` (Claude Pro/Max) and `openai-codex` (ChatGPT
4
+ * Plus/Pro). Each provider is overlaid with a `refreshModels` hook that asks
5
+ * the subscription's own model endpoint what the account can actually use, so
6
+ * newly released models appear in the picker without a CLI update.
7
+ *
8
+ * The engine's `ModelsImpl.refresh` runs every provider through two phases:
9
+ * an offline phase carrying the stored credential, then — when the refresh
10
+ * allows network — an online phase with the resolved credential. The overlay
11
+ * replaces the whole visible catalog when it returns an array, so each phase
12
+ * rebuilds the discovered list the same way:
13
+ *
14
+ * online — fetch the endpoint, merge discovered ids over the stored
15
+ * catalog, and persist the discovery to a credential-keyed cache.
16
+ * offline — restore the last cached discovery for the same credential so
17
+ * previously discovered models do not vanish between refreshes.
18
+ * A credential that changed since the last discovery (a rotated
19
+ * OAuth refresh token) keeps the cached discovery usable for a
20
+ * bounded migration window.
21
+ *
22
+ * Returning `undefined` leaves the builtin/remote catalog untouched.
23
+ */
24
+ import { createHash } from 'node:crypto';
25
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
26
+ import { dirname, join, resolve } from 'node:path';
27
+ import { catalogModels } from './model-catalog.mjs';
28
+
29
+ export const ANTHROPIC_PROVIDER_ID = 'anthropic';
30
+ export const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
31
+ export const SUBSCRIPTION_PROVIDER_IDS = [ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID];
32
+
33
+ const ANTHROPIC_BASE_URL = 'https://api.anthropic.com';
34
+ const ANTHROPIC_MODELS_PATH = '/v1/models';
35
+ const ANTHROPIC_VERSION = '2023-06-01';
36
+ // Same beta flag the engine sends on OAuth requests; required for
37
+ // subscription tokens on the platform endpoints.
38
+ const ANTHROPIC_OAUTH_BETA = 'oauth-2025-04-20';
39
+ const ANTHROPIC_MAX_PAGES = 5;
40
+
41
+ const CODEX_BASE_URL = 'https://chatgpt.com/backend-api';
42
+ const CODEX_MODELS_PATH = '/codex/models';
43
+ const CODEX_JWT_CLAIM_PATH = 'https://api.openai.com/auth';
44
+ // Matches the engine's codex-responses requests.
45
+ const CODEX_ORIGINATOR = 'pi';
46
+
47
+ const DISCOVERY_TIMEOUT_MS = 10_000;
48
+ const CACHE_FILE_NAME = 'subscription-models.json';
49
+ // How long a discovery cached under a previous credential stays usable after
50
+ // the stored credential changed (rotated OAuth refresh token). Bounds how
51
+ // long an unrelated credential can inherit the previous credential's
52
+ // discovery before the cache is rejected again.
53
+ const CACHE_MIGRATION_GRACE_MS = 24 * 60 * 60 * 1000;
54
+
55
+ function discoverySignal(signal, timeoutMs) {
56
+ const timeout = AbortSignal.timeout(timeoutMs ?? DISCOVERY_TIMEOUT_MS);
57
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
58
+ }
59
+
60
+ async function readJson(response, label) {
61
+ if (!response.ok) {
62
+ const detail = String(await response.text()).trim().slice(0, 240);
63
+ throw new Error(`${label} model discovery failed (${response.status})${detail ? `: ${detail}` : ''}`);
64
+ }
65
+ return response.json();
66
+ }
67
+
68
+ function sha256(value) {
69
+ return createHash('sha256').update(String(value)).digest('hex');
70
+ }
71
+
72
+ /**
73
+ * Stable fingerprint of the credential the discovery belongs to. The cache is
74
+ * only restored for the same account/key, so models from another subscription
75
+ * are never shown. OAuth prefers `accountId`, then the refresh token (stable
76
+ * across access-token rotation); API keys hash the key itself.
77
+ *
78
+ * Rotation of the refresh token changes the key; `matchCachedDiscovery`
79
+ * keeps the previous discovery usable for a bounded migration window so the
80
+ * rotation does not strand it during offline refreshes.
81
+ */
82
+ export function credentialKey(credential) {
83
+ if (credential?.type === 'oauth') {
84
+ if (typeof credential.accountId === 'string' && credential.accountId) return `oauth:${credential.accountId}`;
85
+ if (typeof credential.refresh === 'string' && credential.refresh) return `oauth:rt:${sha256(credential.refresh)}`;
86
+ if (typeof credential.access === 'string' && credential.access) return `oauth:at:${sha256(credential.access)}`;
87
+ return null;
88
+ }
89
+ if (credential?.type === 'api_key' && typeof credential.key === 'string' && credential.key) {
90
+ return `key:${sha256(credential.key)}`;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /** ChatGPT account id is a JWT claim; the stored codex credential also carries it. */
96
+ export function decodeChatGptAccountId(token) {
97
+ try {
98
+ const claims = JSON.parse(Buffer.from(String(token || '').split('.')[1] || '', 'base64url').toString('utf8'));
99
+ const accountId = claims?.[CODEX_JWT_CLAIM_PATH]?.chatgpt_account_id;
100
+ return typeof accountId === 'string' && accountId ? accountId : null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * GET api.anthropic.com/v1/models — works for both API keys (x-api-key) and
108
+ * Claude subscription OAuth tokens (Bearer + oauth beta).
109
+ */
110
+ export async function fetchAnthropicSubscriptionModels(credential, { baseUrl = ANTHROPIC_BASE_URL, fetchImpl = globalThis.fetch, signal } = {}) {
111
+ if (typeof fetchImpl !== 'function') throw new Error('Anthropic model discovery requires fetch support.');
112
+ const headers = { accept: 'application/json', 'anthropic-version': ANTHROPIC_VERSION };
113
+ if (credential?.type === 'oauth') {
114
+ headers.authorization = `Bearer ${credential.access}`;
115
+ headers['anthropic-beta'] = ANTHROPIC_OAUTH_BETA;
116
+ } else if (credential?.type === 'api_key') {
117
+ headers['x-api-key'] = credential.key;
118
+ } else {
119
+ return { models: [], hiddenIds: new Set() };
120
+ }
121
+ const models = [];
122
+ let afterId = '';
123
+ for (let page = 0; page < ANTHROPIC_MAX_PAGES; page++) {
124
+ const url = new URL(`${String(baseUrl).replace(/\/+$/, '')}${ANTHROPIC_MODELS_PATH}`);
125
+ url.searchParams.set('limit', '1000');
126
+ if (afterId) url.searchParams.set('after_id', afterId);
127
+ const payload = await readJson(await fetchImpl(url, { headers, signal }), 'Anthropic');
128
+ models.push(...parseAnthropicModels(payload));
129
+ if (!payload?.has_more || typeof payload?.last_id !== 'string' || !payload.last_id) break;
130
+ afterId = payload.last_id;
131
+ }
132
+ return { models, hiddenIds: new Set() };
133
+ }
134
+
135
+ export function parseAnthropicModels(payload) {
136
+ const data = Array.isArray(payload?.data) ? payload.data : [];
137
+ const seen = new Set();
138
+ const models = [];
139
+ for (const entry of data) {
140
+ const id = typeof entry?.id === 'string' ? entry.id.trim() : '';
141
+ if (!id || seen.has(id) || (entry.type !== undefined && entry.type !== 'model')) continue;
142
+ seen.add(id);
143
+ const name = typeof entry.display_name === 'string' && entry.display_name.trim() ? entry.display_name.trim() : id;
144
+ models.push({ id, name });
145
+ }
146
+ return models;
147
+ }
148
+
149
+ /**
150
+ * GET chatgpt.com/backend-api/codex/models — the same endpoint the Codex CLI
151
+ * reads for the account's model list. OAuth only.
152
+ */
153
+ export async function fetchCodexSubscriptionModels(credential, { baseUrl = CODEX_BASE_URL, fetchImpl = globalThis.fetch, signal, clientVersion } = {}) {
154
+ if (credential?.type !== 'oauth' || typeof credential.access !== 'string' || !credential.access) {
155
+ return { models: [], hiddenIds: new Set() };
156
+ }
157
+ if (typeof fetchImpl !== 'function') throw new Error('OpenAI model discovery requires fetch support.');
158
+ const headers = {
159
+ accept: 'application/json',
160
+ authorization: `Bearer ${credential.access}`,
161
+ originator: CODEX_ORIGINATOR,
162
+ };
163
+ const accountId = (typeof credential.accountId === 'string' && credential.accountId) || decodeChatGptAccountId(credential.access);
164
+ if (accountId) headers['chatgpt-account-id'] = accountId;
165
+ const url = new URL(`${String(baseUrl).replace(/\/+$/, '')}${CODEX_MODELS_PATH}`);
166
+ if (clientVersion) url.searchParams.set('client_version', clientVersion);
167
+ const payload = await readJson(await fetchImpl(url, { headers, signal }), 'OpenAI');
168
+ return parseCodexModels(payload);
169
+ }
170
+
171
+ export function parseCodexModels(payload) {
172
+ const entries = Array.isArray(payload?.models) ? payload.models
173
+ : Array.isArray(payload?.data) ? payload.data : [];
174
+ const seen = new Set();
175
+ const hiddenIds = new Set();
176
+ const models = [];
177
+ for (const entry of entries) {
178
+ const rawId = typeof entry === 'string' ? entry : entry?.slug ?? entry?.id;
179
+ const id = typeof rawId === 'string' ? rawId.trim() : '';
180
+ if (!id || seen.has(id)) continue;
181
+ seen.add(id);
182
+ if (entry && typeof entry === 'object' && entry.visibility === 'hide') {
183
+ hiddenIds.add(id);
184
+ continue;
185
+ }
186
+ const display = typeof entry === 'object' ? entry.display_name ?? entry.name : undefined;
187
+ const levels = Array.isArray(entry?.supported_reasoning_levels)
188
+ ? entry.supported_reasoning_levels.filter(level => typeof level === 'string')
189
+ : undefined;
190
+ models.push({ id, name: typeof display === 'string' && display.trim() ? display.trim() : id, reasoningLevels: levels });
191
+ }
192
+ return { models, hiddenIds };
193
+ }
194
+
195
+ /** Engine thinking levels derived from the codex endpoint's supported_reasoning_levels. */
196
+ export function codexThinkingLevelMap(levels) {
197
+ if (!Array.isArray(levels) || !levels.length) return undefined;
198
+ const supported = new Set(levels);
199
+ const map = { off: supported.has('none') || supported.has('off') ? 'none' : null };
200
+ for (const level of ['minimal', 'low', 'medium', 'high', 'xhigh', 'max']) {
201
+ map[level] = supported.has(level) ? level : null;
202
+ }
203
+ return map;
204
+ }
205
+
206
+ /**
207
+ * Full model definition for a discovered id. `base` is the richest definition
208
+ * Carthagent knows for this id (owned catalog first, then persisted metadata),
209
+ * so a discovered model keeps its real cost,
210
+ * context window, and thinking levels instead of the conservative fallbacks.
211
+ */
212
+ export function anthropicModelDefinition(model, base) {
213
+ return {
214
+ api: 'anthropic-messages',
215
+ baseUrl: ANTHROPIC_BASE_URL,
216
+ reasoning: true,
217
+ input: ['text', 'image'],
218
+ contextWindow: 200_000,
219
+ maxTokens: 64_000,
220
+ compat: { supportsStrictTools: true },
221
+ ...base,
222
+ id: model.id,
223
+ name: base?.name ?? model.name ?? model.id,
224
+ };
225
+ }
226
+
227
+ export function codexModelDefinition(model, base) {
228
+ const definition = {
229
+ api: 'openai-codex-responses',
230
+ baseUrl: CODEX_BASE_URL,
231
+ reasoning: true,
232
+ input: ['text', 'image'],
233
+ contextWindow: 272_000,
234
+ maxTokens: 128_000,
235
+ compat: { supportsOpenAIGrammarTools: true },
236
+ thinkingLevelMap: codexThinkingLevelMap(model.reasoningLevels),
237
+ ...base,
238
+ id: model.id,
239
+ name: base?.name ?? model.name ?? model.id,
240
+ };
241
+ if (!definition.thinkingLevelMap) delete definition.thinkingLevelMap;
242
+ return definition;
243
+ }
244
+
245
+ /**
246
+ * Discovered models first (the subscription's authoritative order), then any
247
+ * stored catalog entries the endpoint did not return so aliases and remote
248
+ * catalog extras stay visible. `excludeIds` drops stored entries the endpoint
249
+ * now reports hidden.
250
+ */
251
+ export function mergeDiscoveredModels(storedModels, discovered, { excludeIds } = {}) {
252
+ const merged = [...discovered];
253
+ const seen = new Set(discovered.map(model => model.id));
254
+ for (const model of Array.isArray(storedModels) ? storedModels : []) {
255
+ if (model && typeof model.id === 'string' && !seen.has(model.id) && !excludeIds?.has(model.id)) {
256
+ seen.add(model.id);
257
+ merged.push(model);
258
+ }
259
+ }
260
+ return merged;
261
+ }
262
+
263
+ /**
264
+ * One discovery epoch: the credential it was made under, when, and what the
265
+ * endpoint returned. Epochs with a changed credential may still be served
266
+ * within the migration window (see `matchCachedDiscovery`).
267
+ */
268
+ function asDiscoveryEpoch(value) {
269
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
270
+ if (typeof value.credential !== 'string' || !value.credential) return null;
271
+ if (!Array.isArray(value.discovered) || !value.discovered.length) return null;
272
+ return value;
273
+ }
274
+
275
+ function epochIsFresh(epoch, now, graceMs) {
276
+ return graceMs > 0
277
+ && typeof epoch.checkedAt === 'number'
278
+ && Number.isFinite(epoch.checkedAt)
279
+ && now - epoch.checkedAt <= graceMs;
280
+ }
281
+
282
+ /**
283
+ * Resolve the discovery epoch a credential may reuse from a cache entry.
284
+ *
285
+ * An exact credential match always wins, regardless of age. Otherwise the
286
+ * stored credential changed without an intervening successful discovery — a
287
+ * rotated refresh token is the common cause — and the last discovery stays
288
+ * usable for a bounded migration window: either because a later discovery
289
+ * under the rotated credential demoted it to `previous`, or via the primary
290
+ * epoch itself. The window bounds how long an unrelated credential (a
291
+ * different account signed in before its own online discovery) can inherit
292
+ * the cached models; once it lapses the cache is rejected and the catalog
293
+ * falls back to stored/builtin entries only.
294
+ */
295
+ export function matchCachedDiscovery(entry, key, { now = Date.now(), graceMs = CACHE_MIGRATION_GRACE_MS } = {}) {
296
+ const primary = asDiscoveryEpoch(entry);
297
+ const previous = asDiscoveryEpoch(entry?.previous);
298
+ if (previous && previous.credential === key && epochIsFresh(previous, now, graceMs)) return previous;
299
+ if (primary && entry.credential === key) return primary;
300
+ if (primary && epochIsFresh(primary, now, graceMs)) return primary;
301
+ return null;
302
+ }
303
+
304
+ /**
305
+ * Lineage carried into the next cache write. When the credential changed
306
+ * since the last successful discovery (rotated refresh token), the current
307
+ * epoch is demoted to `previous` so `matchCachedDiscovery` can still serve it
308
+ * after the rotation; a same-credential re-discovery keeps any existing
309
+ * lineage. Lineage is one generation deep.
310
+ */
311
+ function previousDiscoveryEpoch(current, key) {
312
+ const epoch = asDiscoveryEpoch(current);
313
+ if (!epoch) return undefined;
314
+ if (epoch.credential === key) return asDiscoveryEpoch(epoch.previous) ?? undefined;
315
+ return {
316
+ credential: epoch.credential,
317
+ checkedAt: epoch.checkedAt,
318
+ discovered: epoch.discovered,
319
+ hiddenIds: epoch.hiddenIds,
320
+ };
321
+ }
322
+
323
+ // Read-modify-write sections per cache file, shared by every cache instance
324
+ // in this process. The engine refreshes provider overlays concurrently, and
325
+ // each write replaces the whole file: without serialization two refreshes can
326
+ // both read the same snapshot and the later write erases the earlier
327
+ // provider's entry. Writes go through a temp file + rename, so readers — and
328
+ // other processes sharing the agent directory — never observe a torn file.
329
+ const cacheWriteQueues = new Map();
330
+
331
+ function serializeCacheWrite(file, operation) {
332
+ const queueKey = resolve(file);
333
+ const previous = cacheWriteQueues.get(queueKey) ?? Promise.resolve();
334
+ const next = previous.then(operation, operation);
335
+ cacheWriteQueues.set(queueKey, next.catch(() => {}));
336
+ return next;
337
+ }
338
+
339
+ async function writeJsonAtomically(file, contents) {
340
+ await mkdir(dirname(file), { recursive: true });
341
+ const temp = `${file}.${process.pid}-${Date.now()}.tmp`;
342
+ try {
343
+ await writeFile(temp, contents);
344
+ await rename(temp, file);
345
+ } catch (error) {
346
+ await rm(temp, { force: true }).catch(() => {});
347
+ throw error;
348
+ }
349
+ }
350
+
351
+ /** File-backed discovery cache, one entry per provider keyed by credential. */
352
+ export function fileSubscriptionCache(agentDir) {
353
+ if (!agentDir) return undefined;
354
+ const file = join(agentDir, CACHE_FILE_NAME);
355
+ const readAll = async () => {
356
+ try {
357
+ const parsed = JSON.parse(await readFile(file, 'utf8'));
358
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
359
+ } catch {
360
+ return {};
361
+ }
362
+ };
363
+ return {
364
+ file,
365
+ async read(providerId) {
366
+ const entry = (await readAll())[providerId];
367
+ return entry && typeof entry === 'object' ? entry : null;
368
+ },
369
+ async write(providerId, entry) {
370
+ return serializeCacheWrite(file, async () => {
371
+ const all = await readAll();
372
+ all[providerId] = entry;
373
+ await writeJsonAtomically(file, JSON.stringify(all, null, 2));
374
+ });
375
+ },
376
+ };
377
+ }
378
+
379
+ async function knownModelsById(providerId, knownModels) {
380
+ const loader = knownModels ?? catalogModels;
381
+ const entries = await loader(providerId);
382
+ return new Map((Array.isArray(entries) ? entries : []).filter(model => model?.id).map(model => [model.id, model]));
383
+ }
384
+
385
+ function envApiKeyCredential(name) {
386
+ const key = process.env[name];
387
+ return typeof key === 'string' && key.trim() ? { type: 'api_key', key: key.trim() } : undefined;
388
+ }
389
+
390
+ async function subscriptionModelRefresh(context, { providerId, fetchModels, toDefinition, knownModels, cache, cacheGraceMs, offlineCredential }) {
391
+ if (context?.signal?.aborted) return undefined;
392
+ // Offline refreshes only carry the stored credential; env API keys reach the
393
+ // hook through `offlineCredential` so the cache can still be keyed to them.
394
+ const credential = context.credential ?? (context.allowNetwork ? undefined : offlineCredential?.());
395
+ const key = credentialKey(credential);
396
+ if (!key) return undefined;
397
+ const stored = Array.isArray(context.stored?.models) ? context.stored.models : [];
398
+ const storedById = new Map(stored.filter(model => model?.id).map(model => [model.id, model]));
399
+
400
+ if (!context.allowNetwork) {
401
+ const cached = await cache?.read(providerId).catch(() => null);
402
+ const epoch = matchCachedDiscovery(cached, key, { graceMs: cacheGraceMs });
403
+ if (!epoch) return undefined;
404
+ const knownById = await knownModelsById(providerId, knownModels);
405
+ const defs = epoch.discovered.map(model => toDefinition(model, knownById.get(model.id) ?? storedById.get(model.id)));
406
+ return mergeDiscoveredModels(stored, defs, { excludeIds: new Set(epoch.hiddenIds ?? []) });
407
+ }
408
+
409
+ const result = await fetchModels(credential);
410
+ const discovered = Array.isArray(result?.models) ? result.models : [];
411
+ if (context.signal?.aborted || !discovered.length) return undefined;
412
+ const hiddenIds = result.hiddenIds instanceof Set ? result.hiddenIds : new Set(result.hiddenIds ?? []);
413
+ const knownById = await knownModelsById(providerId, knownModels);
414
+ const defs = discovered.map(model => toDefinition(model, knownById.get(model.id) ?? storedById.get(model.id)));
415
+ const previous = previousDiscoveryEpoch(await cache?.read(providerId).catch(() => null), key);
416
+ await cache?.write(providerId, {
417
+ credential: key,
418
+ checkedAt: Date.now(),
419
+ discovered,
420
+ hiddenIds: [...hiddenIds],
421
+ previous,
422
+ }).catch(() => {});
423
+ return mergeDiscoveredModels(stored, defs, { excludeIds: hiddenIds });
424
+ }
425
+
426
+ /** Overlay config for the builtin `anthropic` provider (subscription + API key). */
427
+ export function anthropicDiscoveryProviderConfig({ baseUrl = ANTHROPIC_BASE_URL, fetchImpl = globalThis.fetch, timeoutMs, knownModels, cache, cacheGraceMs } = {}) {
428
+ return {
429
+ api: 'anthropic-messages',
430
+ baseUrl,
431
+ models: catalogModels(ANTHROPIC_PROVIDER_ID),
432
+ refreshModels: context => subscriptionModelRefresh(context, {
433
+ providerId: ANTHROPIC_PROVIDER_ID,
434
+ fetchModels: credential => fetchAnthropicSubscriptionModels(credential, {
435
+ baseUrl,
436
+ fetchImpl,
437
+ signal: discoverySignal(context.signal, timeoutMs),
438
+ }),
439
+ toDefinition: anthropicModelDefinition,
440
+ knownModels,
441
+ cache,
442
+ cacheGraceMs,
443
+ offlineCredential: () => envApiKeyCredential('ANTHROPIC_API_KEY'),
444
+ }),
445
+ };
446
+ }
447
+
448
+ /** Overlay config for the builtin `openai-codex` provider (ChatGPT subscription). */
449
+ export function codexDiscoveryProviderConfig({ baseUrl = CODEX_BASE_URL, fetchImpl = globalThis.fetch, timeoutMs, clientVersion, knownModels, cache, cacheGraceMs } = {}) {
450
+ return {
451
+ api: 'openai-codex-responses',
452
+ baseUrl,
453
+ models: catalogModels(OPENAI_CODEX_PROVIDER_ID),
454
+ refreshModels: context => subscriptionModelRefresh(context, {
455
+ providerId: OPENAI_CODEX_PROVIDER_ID,
456
+ fetchModels: credential => fetchCodexSubscriptionModels(credential, {
457
+ baseUrl,
458
+ fetchImpl,
459
+ clientVersion,
460
+ signal: discoverySignal(context.signal, timeoutMs),
461
+ }),
462
+ toDefinition: codexModelDefinition,
463
+ knownModels,
464
+ cache,
465
+ cacheGraceMs,
466
+ }),
467
+ };
468
+ }
469
+
470
+ /**
471
+ * Register both overlays on a ModelRuntime (or the extension API, which
472
+ * exposes the same registerProvider signature). `agentDir` enables the
473
+ * credential-keyed on-disk cache used to restore discovered models during
474
+ * offline refreshes; `cacheGraceMs` bounds how long a discovery stays usable
475
+ * after its credential changed (defaults to one day).
476
+ */
477
+ export function registerSubscriptionProviders(runtime, options = {}) {
478
+ if (!runtime || typeof runtime.registerProvider !== 'function') throw new TypeError('A ModelRuntime is required.');
479
+ const shared = { ...options, cache: options.cache ?? fileSubscriptionCache(options.agentDir) };
480
+ runtime.registerProvider(ANTHROPIC_PROVIDER_ID, anthropicDiscoveryProviderConfig(shared));
481
+ runtime.registerProvider(OPENAI_CODEX_PROVIDER_ID, codexDiscoveryProviderConfig(shared));
482
+ return runtime;
483
+ }
@@ -0,0 +1,80 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, openSync, closeSync, unlinkSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { atomicJson, planD2WithProgress } from './delivery.mjs';
5
+
6
+ import { activeReportPath } from './workspace.mjs';
7
+
8
+ export function latestReport(cwd) {
9
+ const selected = activeReportPath(cwd);
10
+ if (selected) return { path: selected, dir: dirname(selected), mtimeMs: statSync(selected).mtimeMs, state: JSON.parse(readFileSync(selected, 'utf8')) };
11
+ const harness = join(cwd, '.harness');
12
+ if (!existsSync(harness)) return null;
13
+ let latest = null;
14
+ for (const session of safeDirectories(harness)) {
15
+ for (const run of safeDirectories(join(harness, session))) {
16
+ const path = join(harness, session, run, 'report.json');
17
+ try {
18
+ const mtimeMs = statSync(path).mtimeMs;
19
+ if (!latest || mtimeMs >= latest.mtimeMs) latest = { path, dir: dirname(path), mtimeMs };
20
+ } catch { }
21
+ }
22
+ }
23
+ if (!latest) return null;
24
+ try { return { ...latest, state: JSON.parse(readFileSync(latest.path, 'utf8')) }; }
25
+ catch { return null; }
26
+ }
27
+
28
+ export function createReport(cwd, state, owner = 'web') {
29
+ const runId = state.runId || randomUUID();
30
+ const dir = join(cwd, '.harness', owner, runId);
31
+ mkdirSync(dir, { recursive: true });
32
+ const report = { ...state, runId };
33
+ const path = join(dir, 'report.json');
34
+ saveReport(path, report);
35
+ return { path, dir, state: report, mtimeMs: statSync(path).mtimeMs };
36
+ }
37
+
38
+ // Reconcile only a restored snapshot of this exact run. Never select by mtime.
39
+ export function reconcileReport(path, state) {
40
+ let current;
41
+ try { current = JSON.parse(readFileSync(path, 'utf8')); }
42
+ catch (error) { if (error.code === 'ENOENT') return state; throw error; }
43
+ if (!state?.runId || current.runId !== state.runId) throw Error(`Report identity mismatch: ${path}`);
44
+ const saved = current.storageVersion ?? 0;
45
+ const restored = state.storageVersion ?? 0;
46
+ if (!Number.isSafeInteger(saved) || saved < 0 || !Number.isSafeInteger(restored) || restored < 0) throw Error(`Invalid report storage version: ${path}`);
47
+ if (saved < restored) throw Error(`Report storage regressed: ${path}`);
48
+ return saved > restored ? current : state;
49
+ }
50
+
51
+ export function saveReport(path, state) {
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ const lock = `${path}.lock`;
54
+ let fd;
55
+ try { fd = openSync(lock, 'wx'); }
56
+ catch (error) {
57
+ if (error.code === 'EEXIST') throw Error(`Report is locked: ${path}. Retry after the writer finishes; remove the lock only after confirming no writer is running.`);
58
+ throw error;
59
+ }
60
+ try {
61
+ const current = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
62
+ const expected = state.storageVersion ?? 0;
63
+ if ((current?.storageVersion ?? 0) !== expected) {
64
+ throw Error(`Report write conflict: ${path}. Reload the current report before retrying.`);
65
+ }
66
+ const next = { ...state, storageVersion: expected + 1 };
67
+ const d2 = state.plan ? planD2WithProgress(state.plan, state.stepStatus || {}) : null;
68
+ atomicJson(path, next);
69
+ state.storageVersion = next.storageVersion;
70
+ if (d2 !== null) writeFileSync(join(dirname(path), 'plan.d2'), d2);
71
+ } finally {
72
+ closeSync(fd);
73
+ unlinkSync(lock);
74
+ }
75
+ }
76
+
77
+ function safeDirectories(path) {
78
+ try { return readdirSync(path, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name); }
79
+ catch { return []; }
80
+ }
@@ -0,0 +1,49 @@
1
+ import { mkdirSync, openSync, closeSync, unlinkSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { atomicJson } from './delivery.mjs';
5
+
6
+ // Local cooperative orchestration, not protection against edits to .harness.
7
+ export function beginReview(cwd, identity, snapshot, head) {
8
+ const dir = join(cwd, '.harness', 'reviews');
9
+ mkdirSync(dir, { recursive: true });
10
+ const path = join(dir, `${createHash('sha256').update(identity).digest('hex')}.json`);
11
+ const lock = `${path}.lock`;
12
+ let fd;
13
+ try { fd = openSync(lock, 'wx'); }
14
+ catch (e) { if (e.code === 'EEXIST') throw Error('Review already running or interrupted; inspect the review lock before manual recovery'); throw e; }
15
+ const release = () => { closeSync(fd); unlinkSync(lock); };
16
+ try {
17
+ let state;
18
+ try { state = JSON.parse(readFileSync(path, 'utf8')); }
19
+ catch (e) { if (e.code !== 'ENOENT') throw e; }
20
+ if (state && (state.version !== 1 || state.identity !== identity || !Number.isInteger(state.rounds) || state.rounds < 1 || state.rounds > 3 || !Array.isArray(state.history) || !['running', 'approved', 'changes-requested', 'inconclusive', 'escalated'].includes(state.status))) throw Error('Invalid persisted review state');
21
+ if (state?.status === 'running') throw Error('Interrupted review requires manual recovery; no automatic retry');
22
+ if (state?.status === 'approved' && state.snapshot === snapshot && state.head === head) throw Error('Snapshot already approved; no reviewer launched');
23
+ if ((state?.rounds || 0) >= 3) throw Error('Review round limit reached; escalate remaining findings to the user');
24
+ state = { version: 1, identity, rounds: (state?.rounds || 0) + 1, status: 'running', snapshot, head, history: state?.history || [] };
25
+ atomicJson(path, state);
26
+ return {
27
+ finish(status, details) {
28
+ state.status = state.rounds === 3 && status !== 'approved' ? 'escalated' : status;
29
+ state.history.push({ round: state.rounds, snapshot, head, status: state.status, details });
30
+ atomicJson(path, state);
31
+ },
32
+ release,
33
+ };
34
+ } catch (e) { release(); throw e; }
35
+ }
36
+
37
+ export function parseFindings(output, snapshot, head, verdict) {
38
+ const lines = String(output).trim().split(/\r?\n/);
39
+ const line = lines.at(-2);
40
+ if (!line?.startsWith('REVIEW_JSON: ')) throw Error('Missing structured REVIEW_JSON findings');
41
+ let data;
42
+ try { data = JSON.parse(line.slice(13)); } catch { throw Error('Invalid REVIEW_JSON'); }
43
+ if (!data || data.version !== 1 || data.snapshot !== snapshot || data.head !== head || !Array.isArray(data.findings) || data.findings.length > 100) throw Error('Invalid or stale structured review identity');
44
+ for (const f of data.findings) {
45
+ if (!f || typeof f.file !== 'string' || !f.file.trim() || !Number.isInteger(f.line) || f.line < 1 || !['blocking', 'nonblocking'].includes(f.severity) || typeof f.issue !== 'string' || !f.issue.trim() || typeof f.fix !== 'string' || !f.fix.trim()) throw Error('Malformed structured finding');
46
+ }
47
+ if ((verdict === 'CHANGES-REQUESTED') !== data.findings.some(f => f.severity === 'blocking')) throw Error('Verdict contradicts structured findings');
48
+ return data;
49
+ }