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,93 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { initializeCampaign } from './campaign.mjs';
7
+ import { runCandidate, terminalStatus } from './runner.mjs';
8
+ import { createFreeze, verifyFreeze, frozenFiles } from './freeze.mjs';
9
+ import { sha } from './runtime.mjs';
10
+ import { nativeFixture } from '../native-fixture.mjs';
11
+ const mk = () => mkdtempSync(join(tmpdir(), 'benchmark-runner-'));
12
+
13
+ test('native terminal classification does not mistake process success for model success', () => {
14
+ assert.equal(terminalStatus([{ type:'session.step.ended', data:{ finish:'error' } }], {code:0}, null, {},'failed').status, 'non-successful-session-outcome');
15
+ assert.equal(terminalStatus([], {code:0}, null, {},'succeeded').status, 'missing-terminal-message');
16
+ });
17
+ test('native runtime freeze includes source and package lock but no Pi install or user auth', () => {
18
+ assert.ok(frozenFiles().some(path => path.endsWith('/lib/opencode/evaluation.mjs')));
19
+ assert.ok(frozenFiles().some(path => path.endsWith('/package-lock.json')));
20
+ assert.ok(frozenFiles().every(path => !path.includes('/.pi/') && !path.includes('/node_modules/')));
21
+ const root = mk(), receipt = join(root,'ready.json'), live = join(root,'live.json');
22
+ writeFileSync(live,'{"passed":true}');
23
+ writeFileSync(receipt,JSON.stringify({passed:true,files:Object.fromEntries(frozenFiles().map(path=>[path,sha(readFileSync(path))])),live:{path:live,sha256:sha(readFileSync(live))}}));
24
+ createFreeze(root,receipt); assert.equal(verifyFreeze(root).runtime,'opencode-2');
25
+ });
26
+ test('offline fixture runner logs runtime and uses no provider credentials', async () => {
27
+ const root=mk(), campaignPath=join(root,'campaign.json');
28
+ initializeCampaign(campaignPath,{historicalCandidate:0,historicalSmoke:0});
29
+ const fake = async options => {
30
+ assert.equal(options.directory,join(root,'smoke-plain','workspace'));
31
+ options.onEvent({type:'session.step.ended',data:{sessionID:'fixture',assistantMessageID:'m1',finish:'stop'}});
32
+ return {exitCode:0,turns:1,productiveTurns:1,tools:{read:1},cost:0,outcome:'succeeded',lastStop:'stop',opencodeVersion:'2.0.32'};
33
+ };
34
+ fake.fixture = true;
35
+ const result=await runCandidate('smoke','plain',{smoke:true,root,campaignPath,nativeRunner:fake});
36
+ assert.equal(result.runtime,'opencode-2');
37
+ assert.equal(result.status,'completed');
38
+ assert.equal(result.outcome,'succeeded');
39
+ assert.equal(JSON.parse(readFileSync(join(root,'smoke-plain','invocation.json'))).runtime,'opencode-2');
40
+ assert.equal(existsSync(join(root,'smoke-plain','config','runtime.json')),true);
41
+ });
42
+
43
+ test('benchmark refuses arbitrary service endpoints before creating a run', async () => {
44
+ const root=mk(), campaignPath=join(root,'campaign.json');
45
+ initializeCampaign(campaignPath,{historicalCandidate:0,historicalSmoke:0});
46
+ const before=process.env.CTG_OPENCODE_URL;
47
+ process.env.CTG_OPENCODE_URL='http://127.0.0.1:1';
48
+ try {
49
+ await assert.rejects(runCandidate('smoke','plain',{smoke:true,root,campaignPath,nativeRunner:Object.assign(async()=>{throw Error('must not execute');},{fixture:true})}),/ownership and authentication/);
50
+ assert.equal(existsSync(join(root,'smoke-plain')),false,'rejected endpoint must be rejected before allocating a run');
51
+ } finally {
52
+ if(before===undefined) delete process.env.CTG_OPENCODE_URL;
53
+ else process.env.CTG_OPENCODE_URL=before;
54
+ }
55
+ });
56
+
57
+ test('native benchmark plugin loads in OpenCode 2 with both session and tool hooks', async () => {
58
+ const root=mk();
59
+ const host=await nativeFixture({
60
+ plugin:join(process.cwd(),'lib/opencode/evaluation-budget-plugin'),
61
+ plugins:[join(process.cwd(),'tools/benchmark/native-plugin'),join(process.cwd(),'plugins/carthagent')],
62
+ env:{BENCH_TOOL_LIMIT:'1',BENCH_RETRY_LIMIT:'0',BENCH_GUARD_LOG:join(root,'guard.jsonl'),BENCH_ADVISOR_LOG:join(root,'advisor.jsonl'),BENCH_GATEWAY:'http://127.0.0.1:1',CTG_EVALUATION_MAX_TURNS:'4',CTG_EVALUATION_MAX_COST:'1'},
63
+ respond(body,res) {
64
+ if(JSON.stringify(body.messages).includes('CTG_PARALLEL_GUARD_PROBE')) {
65
+ const shell=body.tools.find(tool=>tool.function?.name==='shell');
66
+ assert.ok(shell,'native host must advertise shell for the concurrent tool admission probe');
67
+ const calls=Array.from({length:8},(_,index)=>({index,id:`parallel-${index}`,type:'function',function:{name:'shell',arguments:JSON.stringify({command:`printf parallel-${index}`})}}));
68
+ const chunk=(delta,finish_reason=null)=>`data: ${JSON.stringify({id:'parallel-guard-probe',object:'chat.completion.chunk',created:1,model:'fixture',choices:[{index:0,delta,finish_reason}]})}\n\n`;
69
+ res.writeHead(200,{'content-type':'text/event-stream'});
70
+ res.end(`${chunk({role:'assistant',tool_calls:calls})}${chunk({},'tool_calls')}data: [DONE]\n\n`);
71
+ return;
72
+ }
73
+ streamText(res);
74
+ },
75
+ });
76
+ try {
77
+ await host.active('ctg.benchmark');
78
+ const inventory=await host.client.plugin.list({location:host.location},host.request());
79
+ assert.ok(inventory.data.some(plugin=>plugin.id==='ctg.evaluation-budget'&&plugin.state.status==='active'),JSON.stringify(inventory.data));
80
+ const response=await host.client.session.create({location:host.location,model:host.model,title:'hook initialization probe'},host.request());
81
+ assert.ok(response.id);
82
+ const session=await host.client.session.create({location:host.location,model:host.model,title:'parallel admission probe',metadata:{'ctg.evaluation.budget':{maxTurns:4,maxCost:1}}},host.request());
83
+ await host.client.session.prompt({sessionID:session.id,text:'CTG_PARALLEL_GUARD_PROBE'},host.request());
84
+ await host.client.session.wait({sessionID:session.id},host.request());
85
+ assert.ok(existsSync(join(root,'guard.jsonl')),`${host.output}\n${JSON.stringify(host.requests)}`);
86
+ const rows=readFileSync(join(root,'guard.jsonl'),'utf8').trim().split('\n').map(JSON.parse).filter(row=>row.sessionID===session.id);
87
+ assert.equal(rows.length,8,JSON.stringify(rows));
88
+ assert.equal(rows.filter(row=>!row.blocked).length,1,JSON.stringify(rows));
89
+ assert.equal(rows.filter(row=>row.blocked).length,7,JSON.stringify(rows));
90
+ const ledger=await host.client.session.context({sessionID:session.id},host.request());
91
+ assert.ok(ledger.at(-1)?.outcome,JSON.stringify(ledger.at(-1)));
92
+ } finally { await host.close(); }
93
+ });
@@ -0,0 +1,23 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ export const ENGINE_RUNTIME = 'opencode-2';
5
+ export const isolatedHome = config => join(config, 'home');
6
+ export function safeEnv(extra = {}, home = undefined) {
7
+ const allowed = { PATH: process.env.PATH || '', LANG: process.env.LANG || 'C.UTF-8', CI: '1', NODE_ENV: 'production' };
8
+ if (home) { mkdirSync(home, { recursive: true }); allowed.HOME = home; allowed.XDG_CONFIG_HOME = join(home, '.config'); allowed.XDG_DATA_HOME = join(home, '.local/share'); allowed.XDG_CACHE_HOME = join(home, '.cache'); }
9
+ return { ...allowed, ...extra };
10
+ }
11
+ export const sha = value => createHash('sha256').update(value).digest('hex');
12
+ export function provisionTools(config) {
13
+ mkdirSync(config, { recursive: true });
14
+ const receipt = { runtime: ENGINE_RUNTIME, opencodePackage: '@opencode/client@2.0.32', home: isolatedHome(config), noPiDiscovery: true };
15
+ writeFileSync(join(config, 'runtime.json'), JSON.stringify(receipt, null, 2), { flag: 'wx' });
16
+ return receipt;
17
+ }
18
+ export async function stopGroup(child, graceMs = 250) {
19
+ if (!child?.pid) return;
20
+ try { process.kill(-child.pid, 'SIGTERM'); } catch {}
21
+ await new Promise(r => setTimeout(r, graceMs));
22
+ try { process.kill(-child.pid, 'SIGKILL'); } catch {}
23
+ }
@@ -0,0 +1,47 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync,mkdirSync,writeFileSync,readFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { createServer } from 'node:http';
7
+ import { evaluateLedger } from './eval-ledger.mjs';
8
+ import { quoteOracle } from './eval-shop.mjs';
9
+ import { validWorld,changedBlocks } from './eval-voxel.mjs';
10
+ import { browser } from './eval-common.mjs';
11
+ // Independent test-only reference process. Never copied into any candidate workspace.
12
+ const reference=String.raw`
13
+ import fs from 'node:fs';import readline from 'node:readline';
14
+ const file=process.argv[3], balances={},seen=new Map(),events=[];const max=99999999999999999999n;
15
+ const amount=s=>{if(typeof s!=='string'||! /^[+-]?\d+(\.\d{1,2})?$/.test(s))throw Error('decimal');const neg=s[0]==='-';s=s.replace(/^[+-]/,'');const [a,b='']=s.split('.');const n=BigInt(a)*100n+BigInt(b.padEnd(2,'0'));if(n>max)throw Error('range');return neg?-n:n;};
16
+ const fmt=n=>String(n/100n)+'.'+String(n%100n).padStart(2,'0');
17
+ function apply(c,replay=false){const id=s=>{if(typeof s!=='string'||! /^[A-Za-z0-9_-]{1,40}$/.test(s))throw Error('id');return s;};
18
+ if(c.op==='snapshot')return {ok:true,balances:Object.fromEntries(Object.entries(balances).map(([k,n])=>[k,fmt(n)]))};
19
+ if(c.op==='balance'){id(c.account);if(balances[c.account]===undefined)throw Error('missing');return {ok:true,balance:fmt(balances[c.account])};}
20
+ id(c.key);let norm,n;
21
+ if(c.op==='open'){id(c.account);n=amount(c.balance);if(n<0n)throw Error('negative');norm=JSON.stringify(['open',c.account,String(n)]);}
22
+ else if(c.op==='transfer'){id(c.from);id(c.to);n=amount(c.amount);norm=JSON.stringify(['transfer',c.from,c.to,String(n)]);}else throw Error('op');
23
+ if(seen.has(c.key)){if(seen.get(c.key)!==norm)throw Error('conflict');return {ok:true};}
24
+ if(c.op==='open'){if(balances[c.account]!==undefined)throw Error('exists');balances[c.account]=n;}
25
+ else{if(c.from===c.to||n<=0n||balances[c.from]===undefined||balances[c.to]===undefined||balances[c.from]<n||balances[c.to]+n>max)throw Error('transfer');balances[c.from]-=n;balances[c.to]+=n;}
26
+ seen.set(c.key,norm);if(!replay)fs.appendFileSync(file,JSON.stringify(c)+'\n');return {ok:true};}
27
+ try{if(fs.existsSync(file))for(const line of fs.readFileSync(file,'utf8').split('\n').filter(Boolean)){const c=JSON.parse(line);if(!['open','transfer'].includes(c.op))throw Error('record');apply(c,true);}}catch{process.exit(2);}
28
+ for await(const line of readline.createInterface({input:process.stdin})){try{console.log(JSON.stringify(apply(JSON.parse(line))));}catch(e){console.log(JSON.stringify({ok:false,error:e.message}));}}
29
+ `;
30
+ test('ledger evaluator passes reference and detects float/idempotency mutants through real processes',async()=>{
31
+ const root=mkdtempSync(join(tmpdir(),'ledger-oracle-'));writeFileSync(join(root,'ledger.mjs'),reference);
32
+ const positive=await evaluateLedger(root,join(root,'good'));assert.equal(positive.passed,positive.total,JSON.stringify(positive));
33
+ writeFileSync(join(root,'ledger.mjs'),reference.replace('BigInt(a)*100n+BigInt(b.padEnd(2,\'0\'))','BigInt(Math.round(Number(s)*100))'));
34
+ const floating=await evaluateLedger(root,join(root,'float'));assert.ok(floating.results.some(r=>r.name.includes('large magnitudes')&&!r.passed));
35
+ writeFileSync(join(root,'ledger.mjs'),reference.replace("if(seen.has(c.key))","if(false && seen.has(c.key))"));
36
+ const duplicate=await evaluateLedger(root,join(root,'duplicate'));assert.ok(duplicate.results.some(r=>r.name.includes('idempotency')&&!r.passed));
37
+ });
38
+ test('shop oracle catches rounding/shipping mutants and voxel oracle rejects unchanged/malformed state',()=>{
39
+ assert.deepEqual(quoteOracle([{id:'mug',quantity:1}],'WELCOME10'),{subtotalCents:1299,discountCents:129,shippingCents:599,totalCents:1769});
40
+ assert.notEqual(quoteOracle([{id:'mug',quantity:1}],'WELCOME10').discountCents,Math.round(1299*.1));
41
+ assert.equal(quoteOracle([{id:'mat',quantity:2},{id:'mug',quantity:1}],'WELCOME10').shippingCents,599);
42
+ const s={seed:1,player:{x:1,y:3,z:1,yaw:0,pitch:0},blocks:Array.from({length:256},(_,i)=>[i%16,0,Math.floor(i/16),i%3+1])};validWorld(s);assert.equal(changedBlocks(s,structuredClone(s)),0);assert.throws(()=>validWorld({...s,blocks:[]}));assert.throws(()=>validWorld({...s,player:{...s.player,y:0}}));
43
+ });
44
+ test('real Firefox infrastructure and native click screenshot',async()=>{
45
+ const base='/home/baissi/benchmarks/infrastructure-tests';mkdirSync(base,{recursive:true});const root=mkdtempSync(join(base,'browser-'));const server=createServer((req,res)=>{res.setHeader('Content-Type','text/html');res.end('<button onclick="this.textContent=\'clicked\'" id="go">ready</button>');});await new Promise(r=>server.listen(0,'127.0.0.1',r));let b;
46
+ try{b=await browser(root);await b.call('/url',{url:`http://127.0.0.1:${server.address().port}`});const el=await b.call('/element',{using:'css selector',value:'#go'});await b.call('/element/'+el['element-6066-11e4-a52e-4f735466cecf']+'/click',{});assert.equal(await b.exec('document.querySelector("#go").textContent'),'clicked');await b.shot(root+'/smoke.png');assert.equal(readFileSync(root+'/smoke.png').subarray(1,4).toString(),'PNG');}finally{await b?.close();await new Promise(r=>server.close(r));}
47
+ });
@@ -0,0 +1,5 @@
1
+ // Retained entry point; the old standalone smoke bypassed campaign accounting.
2
+ // All new live checks now use the exclusive, centrally accounted preflight.
3
+ import { livePreflight } from './preflight-live.mjs';
4
+ if (process.argv[2] !== '--paid') throw Error('Use node tools/benchmark/preflight-live.mjs --paid (shared smoke budget, no overwrite/retry).');
5
+ console.log(JSON.stringify(await livePreflight()));
@@ -0,0 +1,20 @@
1
+ export const COMMON = `Implement the full task in this initially empty Git workspace. Do not modify TASK.md. Include README launch instructions and meaningful automated tests. Work only within this workspace; do not inspect sibling benchmark runs, supervisor scripts/evaluators or host credentials, and do not call external AI services. Dependencies may be installed. Node 26, Rust 1.98/cargo, cc and Firefox are available. No real payments, deployments or external accounts. Budget: 15 minutes and 120 tool starts, so deliver a runnable vertical slice early. Independent evaluation runs after you finish; you will not receive its source or results during implementation. Avoid leaving development servers running. Final response: list implemented features, commands actually tested and remaining limitations.\n`;
2
+ export const TASKS = {
3
+ voxel: `${COMMON}
4
+ Build a small playable Minecraft-like voxel sandbox in Rust, not merely a data model or static picture. Rust must own terrain, physics, interactions, saves AND first-person software rendering. A thin browser JS input/display frontend is allowed. Do not require GPU/display or downloaded art. Use distinct block colors, sky, depth shading, crosshair, instructions and a visible selected-block indicator. WASD moves, arrow keys turn/look, Space jumps, left click removes and right click places a selected block; keys 1/2/3 choose material. Collisions prevent walking through solid terrain, gravity settles the player, reach <=5 blocks, and placement cannot intersect player. Deterministic finite terrain >=16x16 horizontally, >=8 high, with at least grass/dirt/stone. Same seed => same world; different seeds => different terrain. Save and reload preserve blocks and player; malformed/out-of-bounds saves are rejected atomically.
5
+ Public interface (required for independent evaluation): Cargo binary named voxel. cargo build --release; launch target/release/voxel --port PORT --save FILE (bind 127.0.0.1). Serve browser UI at /. GET /api/state -> JSON {seed,player:{x,y,z,yaw,pitch},selected,blocks:[[x,y,z,material],...]}; occupied blocks only, integer coordinates, materials 1/2/3. POST /api/action JSON actions below returns {ok:true,state:<same shape>} or {ok:false,error:string}; HTTP 4xx is acceptable for rejected actions. GET /api/frame -> Rust-rendered PPM P6 image >=160x100 (browser may convert to canvas). Coordinates: y up, yaw 0 looks +z; pitch 0 horizontal. APIs must power normal browser controls, not evaluator-only stand-ins.
6
+ Actions: {type:"reset",seed:integer}; {type:"look",yaw:number,pitch:number} sets absolute view radians (clamp pitch safely); {type:"move",forward:number,right:number,jump:boolean,dt:number} with forward/right in [-1,1], dt in (0,0.25], move uses current yaw, applies physics; {type:"select",material:1|2|3}; {type:"remove"}; {type:"place"}; {type:"save"}; {type:"load"}. Unknown/invalid actions must not mutate. Finite world boundary must be collision-safe. On reset spawn in free space above terrain, near its center. Include unit tests and HTTP integration tests. No crafting, mobs, infinite streaming or multiplayer required.
7
+ `,
8
+ shop: `${COMMON}
9
+ Build a polished responsive outdoor-products sales website with a real Node backend, no framework required. Launch node server.mjs; PORT and DATA_DIR environment variables choose bind port and persistence directory (127.0.0.1). npm test runs your automated tests. Serve / with semantic labeled controls, keyboard accessibility, visible loading/error/success feedback, attractive desktop/mobile layout, product cards, search/category/sort, cart quantity/remove, promo, checkout form and order confirmation. Use local CSS/SVG/emoji rather than unavailable remote assets. Cart survives page reload via localStorage; server owns pricing, inventory and orders. No actual payment.
10
+ Catalog on first launch, exact fields {id,name,category,priceCents,stock}: tent / Alpine Tent / shelter / 12999 / 3; mug / Trail Mug / accessories / 1299 / 8; lamp / Camp Lantern / lighting / 3499 / 4; pack / Ridge Pack / bags / 7999 / 2; rope / Utility Rope / accessories / 999 / 0; mat / Sleeping Mat / shelter / 4599 / 5. All prices/totals integer cents. WELCOME10 gives floor(subtotalCents/10) discount. Shipping 599 unless post-discount subtotal >=10000, then 0. Unknown nonempty coupon rejected; empty cart rejected. Quantities positive integers, merge duplicate product IDs before validating aggregate stock. UI must not add out-of-stock products.
11
+ Public JSON APIs: GET /api/products?q=<case-insensitive-name-substring>&category=<exact-category>&sort=price-asc|price-desc|name -> {products:[...]}, optional filters compose. POST /api/quote {items:[{id,quantity}],coupon:""} -> {subtotalCents,discountCents,shippingCents,totalCents}. POST /api/orders same plus {key:string,customer:{name,email,address}} -> {order:{id,items,subtotalCents,discountCents,shippingCents,totalCents}}. Trimmed name>=2 chars, conventional nonblank email containing @ with domain dot, trimmed address>=8 chars required. Invalid input returns HTTP 4xx JSON {error:string} with NO mutation. Same key+same normalized purchase/customer request => same order exactly and no stock decrement; same key+different request =>409. GET /api/orders/:id -> {order}. Orders and stock survive backend restart, including idempotency. Check and decrement inventory atomically even for concurrent orders.
12
+ Stable UI test hooks (data-testid values): search (input), category (select), sort (select), products (container), add-<id> (button), cart (container), qty-<id> (numeric input inside cart), remove-<id> (button), coupon (input), total (text with formatted dollars), customer-name/email/address (inputs), checkout (button), status (aria-live status). Updates may be async, <=2 seconds. Cart quantity input change recalculates totals. Include meaningful backend and browser-oriented tests.
13
+ `,
14
+ ledger: `${COMMON}
15
+ Implement an exact-money transfer ledger in Node, using only built-in dependencies. Launch node ledger.mjs --journal PATH. Read one JSON object per stdin line and emit exactly one JSON object per line (no other stdout); process until EOF. Each request is atomic. Persist accepted mutations in append-only JSONL; restart replays balances and idempotency. Missing journal creates empty state. Malformed JSON or invalid records anywhere in existing journal => exit nonzero before reading commands, never truncate/repair it silently. Handle malformed request lines with {ok:false,error:string} and continue.
16
+ Commands: {op:"open",account:string,balance:string,key:string}; {op:"transfer",from:string,to:string,amount:string,key:string}; {op:"balance",account:string}; {op:"snapshot"}. Account/key IDs must match /^[A-Za-z0-9_-]{1,40}$/. open creates a previously absent account with nonnegative balance; transfer requires distinct existing accounts, strictly positive amount, enough funds. Mutations have globally unique idempotency keys: semantically identical retry returns its original result without reapplying; same key with a different normalized command => error and no mutation. Decimal money syntax /^[+-]?\\d+(\\.\\d{1,2})?$/ with no whitespace, exponent or rounding. Leading zeroes and + accepted; negative zero equivalent to zero. Normalize equivalent decimal spellings for idempotency. Amount range absolute <=999999999999999999.99 (cents <=99999999999999999999); reject source/destination overflow atomically. No IEEE-754 parsing/arithmetic for money.
17
+ Responses: mutations {ok:true}, balance {ok:true,balance:"canonical decimal with exactly two places"}, snapshot {ok:true,balances:{account:"canonical amount",...}}, all failures {ok:false,error:nonempty string}. No timestamps/extra nondeterministic fields needed. Unknown operations rejected. Failed mutations neither consume key nor modify durable state; rejected transfer can be retried successfully later using same key. Balances always nonnegative and transfers conserve exact total. Supply npm test with adversarial and restart integration coverage; README describes crash/concurrency limits honestly. One process writer at a time is sufficient; no multi-process locking required.
18
+ `,
19
+ };
20
+ export const MATRIX = [['voxel','plain'],['voxel','harness'],['voxel','jev'],['shop','harness'],['shop','jev'],['shop','plain'],['ledger','jev'],['ledger','plain'],['ledger','harness']];
@@ -0,0 +1,65 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { safeEnv } from './runtime.mjs';
6
+ // Credentials travel through stdin config, never argv or files. Request BODY uses
7
+ // a private 0700 directory / 0600 file: curl's inline config has a line-size limit.
8
+ // No Node TLS / connection reuse; body files are removed on close/spawn failure.
9
+ // Abrupt supervisor termination may leave private files; this is not zero retention.
10
+ export function createCurlTransport({ executable = '/usr/bin/curl', caFile, maxBytes = 4000000, timeoutMs = 180000, allowLoopbackHttp = false } = {}) {
11
+ return async function curlFetch(url, options = {}) {
12
+ const target = new URL(url);
13
+ if (target.protocol !== 'https:' && !(allowLoopbackHttp && target.protocol === 'http:' && target.hostname === '127.0.0.1')) throw Error('HTTPS required');
14
+ if (target.username || target.password) throw Error('URL credentials forbidden');
15
+ options.signal?.throwIfAborted();
16
+ const headers = Object.entries(options.headers || {}).map(([k,v]) => {
17
+ if (!/^[A-Za-z0-9-]+$/.test(k) || /[\r\n\0]/.test(String(v))) throw Error('Invalid HTTP header');
18
+ return `header = ${JSON.stringify(`${k}: ${v}`)}\n`;
19
+ }).join('');
20
+ const directory = mkdtempSync(join(tmpdir(), 'ctg-http-body-'));
21
+ const bodyPath = join(directory, 'body');
22
+ try { writeFileSync(bodyPath, options.body || '', { mode: 0o600 }); }
23
+ catch (error) { rmSync(directory,{recursive:true,force:true}); throw error; }
24
+ const args = ['-q', '--silent', '--show-error', '--http1.1', '--proto', allowLoopbackHttp ? '=http,https' : '=https', '--max-redirs', '0', '--connect-timeout', '15', '--max-time', String(timeoutMs / 1000), '--retry', '0', '--request', options.method || 'POST', '--config', '-', '--data-binary', '@' + bodyPath, '--include', '--write-out', '\n%{http_code}', ...(caFile ? ['--cacert', caFile] : []), '--url', target.href];
25
+ return new Promise((resolve, reject) => {
26
+ const child = spawn(executable, args, { env: safeEnv(), stdio: ['pipe','pipe','pipe'] });
27
+ let chunks = [], bytes = 0, failure;
28
+ const fail = (code) => { failure ||= Object.assign(Error('HTTPS transport failed'), { code }); child.kill('SIGKILL'); };
29
+ const abort = () => fail('ABORTED');
30
+ const timer = setTimeout(() => fail('TRANSPORT_TIMEOUT'), timeoutMs + 1000);
31
+ options.signal?.addEventListener('abort', abort, { once: true });
32
+ child.stdout.on('data', b => { bytes += b.length; if (bytes > maxBytes + 65536 + 4) fail('RESPONSE_TOO_LARGE'); else chunks.push(b); });
33
+ // Drain but do not expose curl stderr, which can include server-controlled content.
34
+ child.stderr.on('data', () => {});
35
+ child.on('error', () => { failure ||= Object.assign(Error('HTTPS transport unavailable'), { code: 'TRANSPORT_SPAWN' }); });
36
+ child.stdin.on('error', () => {});
37
+ child.on('close', code => {
38
+ clearTimeout(timer); options.signal?.removeEventListener('abort', abort);
39
+ rmSync(directory,{recursive:true,force:true});
40
+ if (failure || code !== 0) return reject(failure || Object.assign(Error('HTTPS transport failed'), { code: `CURL_${code}` }));
41
+ const data = Buffer.concat(chunks), status = Number(data.subarray(-3).toString());
42
+ if (data.at(-4) !== 10 || status < 200 || status > 599) return reject(Error('Invalid HTTPS status'));
43
+ let body = data.subarray(0,-4), headerBytes = 0, fields;
44
+ do {
45
+ const end = body.indexOf('\r\n\r\n');
46
+ if (end < 0 || !body.subarray(0,5).equals(Buffer.from('HTTP/'))) return reject(Error('Invalid HTTPS headers'));
47
+ headerBytes += end + 4;
48
+ if (headerBytes > 65536) return reject(Error('HTTPS headers too large'));
49
+ const lines = body.subarray(0,end).toString().split('\r\n');
50
+ const interim = /^HTTP\/\S+ 1\d\d\b/.test(lines[0]) || / 200 Connection established$/i.test(lines[0]);
51
+ fields = new Headers();
52
+ for (const line of lines.slice(1)) { const colon = line.indexOf(':'); if(colon>0 && ['x-request-id','request-id','cf-ray'].includes(line.slice(0,colon).toLowerCase())) fields.set(line.slice(0,colon),line.slice(colon+1).trim()); }
53
+ body = body.subarray(end+4);
54
+ if (!interim) break;
55
+ } while (body.length);
56
+ if (body.length > maxBytes) return reject(Object.assign(Error('HTTPS transport failed'),{code:'RESPONSE_TOO_LARGE'}));
57
+ resolve(new Response([204,205,304].includes(status) ? null : body, { status, headers:fields }));
58
+ });
59
+ // Only short header lines enter curl's config parser. Body is byte-preserving.
60
+ child.stdin.end(headers);
61
+ if (options.signal?.aborted) abort();
62
+ });
63
+ };
64
+ }
65
+ export const curlFetch = createCurlTransport();
@@ -0,0 +1,44 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createServer } from 'node:https';
4
+ import { createServer as httpServer } from 'node:http';
5
+ import { sha } from './runtime.mjs';
6
+ import { mkdtempSync,readFileSync,existsSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { createCurlTransport } from './transport.mjs';
11
+ test('real TLS: trusted local certificate, fresh requests, no retry, redirect, bounds, deadline and cancellation',async()=>{
12
+ const dir=mkdtempSync(join(tmpdir(),'benchmark-tls-')),key=join(dir,'key.pem'),cert=join(dir,'cert.pem');
13
+ const r=spawnSync('openssl',['req','-x509','-newkey','rsa:2048','-nodes','-keyout',key,'-out',cert,'-days','1','-subj','/CN=localhost','-addext','subjectAltName=IP:127.0.0.1'],{encoding:'utf8'});assert.equal(r.status,0,r.stderr);
14
+ let calls=0;const sockets=new Set();
15
+ const server=createServer({key:readFileSync(key),cert:readFileSync(cert)},async(req,res)=>{calls++;sockets.add(req.socket);let body='';for await(const chunk of req)body+=chunk;
16
+ if(req.url==='/stall')return;
17
+ if(req.url==='/redirect'){res.writeHead(302,{location:'/ok'});return res.end();}
18
+ if(req.url==='/big')return res.end('x'.repeat(10000));
19
+ if(req.url==='/disconnect'){req.socket.destroy();return;}
20
+ res.setHeader('x-request-id','tls-fixture');
21
+ assert.equal(req.headers.authorization,'Bearer private-test');res.write(Buffer.from('héllo 🌍').subarray(0,3));res.end(Buffer.concat([Buffer.from('héllo 🌍').subarray(3),Buffer.from(body)]));
22
+ });await new Promise(r=>server.listen(0,'127.0.0.1',r));const url=`https://127.0.0.1:${server.address().port}`;
23
+ const options={method:'POST',headers:{Authorization:'Bearer private-test'},body:'payload'};
24
+ try{
25
+ await assert.rejects(createCurlTransport()(url,options),e=>e.code==='CURL_60');assert.equal(calls,0,'untrusted cert must fail');
26
+ const fetch=createCurlTransport({caFile:cert});
27
+ for(let i=0;i<25;i++)assert.equal(await(await fetch(url,options)).text(),'héllo 🌍payload');assert.equal(sockets.size,25,'fresh TLS connections');
28
+ assert.equal((await fetch(url+'/redirect',options)).status,302);assert.equal(calls,26,'no redirect follow');
29
+ await assert.rejects(fetch(url+'/disconnect',options));assert.equal(calls,27,'no retry of ambiguous POST');
30
+ await assert.rejects(createCurlTransport({caFile:cert,maxBytes:100})(url+'/big',options),e=>e.code==='RESPONSE_TOO_LARGE');
31
+ await assert.rejects(createCurlTransport({caFile:cert,timeoutMs:100})(url+'/stall',options));
32
+ const ac=new AbortController();setTimeout(()=>ac.abort(),50);await assert.rejects(fetch(url+'/stall',{...options,signal:ac.signal}),e=>e.code==='ABORTED');
33
+ await assert.rejects(fetch('http://example.com',options),/HTTPS required/);
34
+ await assert.rejects(fetch(url,{headers:{Authorization:'x\nInjected: y'}}),/Invalid HTTP header/);
35
+ await assert.rejects(createCurlTransport({executable:'/missing-curl'})(url,options),e=>e.code==='TRANSPORT_SPAWN');
36
+ const large=JSON.stringify({content:'héllo\\\"\n'.repeat(16000)});const largeResponse=await fetch(url,{...options,body:large});assert.equal(largeResponse.headers.get('x-request-id'),'tls-fixture');assert.equal(await largeResponse.text(),'héllo 🌍'+large);
37
+ }finally{server.closeAllConnections();await new Promise(r=>server.close(r));}
38
+ });
39
+ test('real curl wire body remains byte-exact across former config limits and historical multi-turn request',async()=>{
40
+ const received=[];const server=httpServer(async(req,res)=>{const chunks=[];for await(const c of req)chunks.push(c);const bytes=Buffer.concat(chunks);received.push(bytes);res.end(JSON.stringify({bytes:bytes.length,hash:sha(bytes)}));});await new Promise(r=>server.listen(0,'127.0.0.1',r));
41
+ const send=createCurlTransport({allowLoopbackHttp:true});const payloads=[...Array.from([60000,90000,97377,130000,399000],n=>JSON.stringify({content:'x'.repeat(n)})),JSON.stringify({content:'line\nquote"slash\\tab\té🌍'.repeat(9000)})];
42
+ const historical='/home/baissi/benchmarks/deepseek-jev-v3/http400-diagnostic/payload.json';if(existsSync(historical))payloads.push(readFileSync(historical,'utf8'));
43
+ try{for(const body of payloads){const r=await send(`http://127.0.0.1:${server.address().port}`,{body,headers:{'Content-Type':'application/json'}});const d=await r.json();assert.equal(d.hash,sha(body));assert.equal(d.bytes,Buffer.byteLength(body));assert.deepEqual(JSON.parse(received.at(-1)),JSON.parse(body));}}finally{server.closeAllConnections();await new Promise(r=>server.close(r));}
44
+ });
@@ -0,0 +1,18 @@
1
+ import { createHash } from 'node:crypto';
2
+ // Known credentials are removed BEFORE truncation. This is not general-purpose
3
+ // secret detection: provider errors may echo submitted source or planning data.
4
+ export function redactError(value, secrets = []) {
5
+ let text=String(value);
6
+ for(const secret of secrets.filter(s=>typeof s==='string'&&s.length)){
7
+ const variants=[secret,JSON.stringify(secret).slice(1,-1),encodeURIComponent(secret),Buffer.from(secret).toString('base64')];
8
+ for(const v of variants)text=text.split(v).join('[REDACTED]');
9
+ }
10
+ return text.replace(/Bearer\s+[^\s"'<>]+/gi,'Bearer [REDACTED]').replace(/sk-or-v1-[A-Za-z0-9_-]+/g,'[REDACTED]').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g,'?');
11
+ }
12
+ export async function upstreamError(response,{secrets=[],maxBytes=16384}={}){
13
+ const reader=response.body?.getReader();const chunks=[];let bytes=0,truncated=false;
14
+ try{if(reader)for(;;){const {done,value}=await reader.read();if(done)break;const take=Math.min(value.byteLength,maxBytes-bytes);chunks.push(value.subarray(0,take));bytes+=take;if(value.byteLength>take||bytes===maxBytes){truncated=true;await reader.cancel();break;}}}finally{reader?.releaseLock();}
15
+ const body=Buffer.concat(chunks).toString('utf8');let parsed;try{parsed=JSON.parse(body);}catch{}
16
+ const clean=v=>typeof v==='string'||typeof v==='number'?redactError(v,secrets).slice(0,2000):null;
17
+ return {status:response.status,code:clean(parsed?.error?.code),message:clean(parsed?.error?.message || parsed?.message),provider:clean(parsed?.error?.metadata?.provider_name),requestId:clean(response.headers.get('x-request-id')||parsed?.request_id||parsed?.id),bodyExcerpt:truncated?'[Oversized upstream error omitted]':redactError(body,secrets).slice(0,8000),truncated:truncated||body.length>8000,bodyPrefixSha256:createHash('sha256').update(body).digest('hex')};
18
+ }
@@ -0,0 +1,10 @@
1
+ import test from 'node:test';import assert from 'node:assert/strict';
2
+ import {upstreamError}from'./upstream-error.mjs';import{startGateway,MODEL}from'./gateway.mjs';
3
+ import{reconstruct,inspectRequest}from'./diagnose-request.mjs';import{existsSync}from'node:fs';
4
+ test('bounded error receipts redact reflected credentials and preserve diagnostic fields',async()=>{
5
+ const secret='private+key/test';const r=await upstreamError(Response.json({error:{code:400,message:'Invalid tool history: '+secret,metadata:{provider_name:'fixture',raw:'Bearer token '+encodeURIComponent(secret)}}},{status:400,headers:{'x-request-id':'req123'}}),{secrets:[secret]});assert.equal(r.status,400);assert.equal(r.requestId,'req123');assert.equal(r.provider,'fixture');assert.match(r.message,/Invalid tool history/);assert.ok(!JSON.stringify(r).includes(secret));assert.ok(!JSON.stringify(r).includes(encodeURIComponent(secret)));
6
+ let canceled=false;const over=await upstreamError(new Response(new ReadableStream({start(c){c.enqueue(new TextEncoder().encode('x'.repeat(17000)));},cancel(){canceled=true;}}),{status:400}));assert.equal(canceled,true);assert.equal(over.bodyExcerpt,'[Oversized upstream error omitted]');
7
+ const html=await upstreamError(new Response('<html>bad request</html>',{status:400}));assert.match(html.bodyExcerpt,/bad request/);
8
+ });
9
+ test('gateway records upstream rejection without replay or credential leak',async()=>{let calls=0;const g=await startGateway({key:'reflected-secret',fetchImpl:async()=>{calls++;return Response.json({error:{code:400,message:'Invalid input reflected-secret'}},{status:400});}});try{for(let i=0;i<2;i++){const r=await fetch(g.url+'/v1/chat/completions',{method:'POST',body:JSON.stringify({model:MODEL,messages:[]})});await r.text();assert.equal(r.status,400);}assert.equal(calls,1);assert.match(g.receipts.find(r=>r.type==='http-failure').message,/Invalid input/);assert.ok(!JSON.stringify(g.receipts).includes('reflected-secret'));assert.equal(g.budget.reserved,.15);}finally{await g.close();}});
10
+ test('offline reconstruction matches original failed multi-turn wire payload, detects orphan results',async(t)=>{const run='/home/baissi/benchmarks/deepseek-jev-v3/voxel-plain';if(!existsSync(run)){t.skip('Historical session not installed');return;}const {payload,report}=await reconstruct(run);assert.equal(report.hashMatches,true);assert.equal(report.toolCalls,11);assert.equal(report.toolResults,11);assert.deepEqual(report.issues,[]);payload.messages.push({role:'tool',tool_call_id:'missing',content:'x'});assert.ok(inspectRequest(payload).issues.some(s=>s.includes('unmatched')));});
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * browser-check — smoke + DOM + screenshot checks for delivered web pages.
4
+ *
5
+ * Declared in delivery_plan checks argv, e.g.:
6
+ * ["node","tools/browser-check.mjs","--page","index.html",
7
+ * "--assert","#keyboard button","--assert-count","#keyboard button:26",
8
+ * "--click","#key-a","--then-text","#word:_A",
9
+ * "--console-clean","--screenshot","artifacts/shot.png"]
10
+ *
11
+ * Serves the workspace over a local HTTP server (avoids file:// CORS),
12
+ * runs jsdom assertions with real inline-script execution, and optionally
13
+ * captures a real screenshot into artifacts/ using the bundled headless
14
+ * shell (auto-downloaded), a system Chrome/Chromium, or Firefox as a last
15
+ * resort.
16
+ *
17
+ * Exit 0 = all assertions pass (screenshot failures are warnings unless
18
+ * --require-screenshot is set). Prints a JSON summary to stdout.
19
+ */
20
+ import { resolve, isAbsolute } from 'node:path';
21
+ import { mkdirSync } from 'node:fs';
22
+ import { dirname } from 'node:path';
23
+ import { serveDir, domCheck, browserScreenshot } from '../lib/browser.mjs';
24
+ import { findBrowser, ensureBundledBrowser } from '../lib/browser-bin.mjs';
25
+
26
+ const args = process.argv.slice(2);
27
+ const opt = (name, def) => {
28
+ const i = args.indexOf(name);
29
+ return i === -1 ? def : args[i + 1];
30
+ };
31
+ const optAll = name => {
32
+ const out = [];
33
+ for (let i = 0; i < args.length; i++) if (args[i] === name && args[i + 1]) out.push(args[i + 1]);
34
+ return out;
35
+ };
36
+ const has = name => args.includes(name);
37
+
38
+ const page = opt('--page', 'index.html');
39
+ const root = resolve(opt('--root', '.'));
40
+ const assertions = [];
41
+
42
+ for (const s of optAll('--assert')) assertions.push({ selector: s });
43
+ for (const s of optAll('--assert-count')) {
44
+ const m = s.match(/^(.+):(\d+)$/);
45
+ if (!m) { console.error(`bad --assert-count: ${s} (want "selector:N")`); process.exit(2); }
46
+ assertions.push({ selector: m[1], count: +m[2] });
47
+ }
48
+ for (const s of optAll('--assert-text')) {
49
+ const m = s.match(/^([^:]+):(.+)$/s);
50
+ if (!m) { console.error(`bad --assert-text: ${s} (want "selector:text")`); process.exit(2); }
51
+ assertions.push({ selector: m[1], text: m[2] });
52
+ }
53
+ // --click '#a' --then-text '#word:_A' — click then assert text on another element
54
+ const clicks = optAll('--click');
55
+ const thenTexts = optAll('--then-text');
56
+ if (clicks.length) {
57
+ clicks.forEach((sel, i) => {
58
+ const t = thenTexts[i];
59
+ const m = t && t.match(/^([^:]+):(.+)$/s);
60
+ assertions.push(m
61
+ ? { action: 'click', selector: sel, then: { selector: m[1], text: m[2] } }
62
+ : { action: 'click', selector: sel, then: { selector: sel } });
63
+ });
64
+ }
65
+ for (const s of optAll('--eval')) assertions.push({ eval: s });
66
+ if (has('--console-clean')) assertions.push({ console: 'error-free' });
67
+
68
+ const srv = await serveDir(root);
69
+ const url = `${srv.url}/${page}`;
70
+ let summary = { url, http: null, dom: null, screenshot: null };
71
+
72
+ try {
73
+ const res = await fetch(url);
74
+ const body = await res.text();
75
+ summary.http = { status: res.status, bytes: body.length, contentType: res.headers.get('content-type') };
76
+ if (res.status !== 200) {
77
+ console.log(JSON.stringify({ ...summary, pass: false, error: `HTTP ${res.status}` }, null, 2));
78
+ process.exit(1);
79
+ }
80
+
81
+ if (assertions.length) {
82
+ const r = await domCheck(body, assertions, { url });
83
+ summary.dom = { pass: r.pass, failures: r.failures, consoleErrors: r.consoleErrors.slice(0, 5) };
84
+ }
85
+
86
+ const shot = opt('--screenshot');
87
+ if (shot) {
88
+ const abs = isAbsolute(shot) ? shot : resolve(root, shot);
89
+ mkdirSync(dirname(abs), { recursive: true });
90
+ const wait = opt('--wait');
91
+ const win = (opt('--window', '1280x800').split('x').map(Number));
92
+ const browser = findBrowser() ?? await ensureBundledBrowser().catch(() => null);
93
+ summary.screenshot = await browserScreenshot(url, abs, {
94
+ width: win[0] || 1280, height: win[1] || 800,
95
+ waitSeconds: wait != null ? +wait : undefined,
96
+ browser,
97
+ });
98
+ if (!summary.screenshot.ok && has('--require-screenshot')) {
99
+ console.log(JSON.stringify({ ...summary, pass: false }, null, 2));
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ const pass = (summary.dom ? summary.dom.pass : true);
105
+ console.log(JSON.stringify({ ...summary, pass }, null, 2));
106
+ process.exit(pass ? 0 : 1);
107
+ } finally {
108
+ srv.close();
109
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { readdirSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ const root = resolve(import.meta.dirname, '..');
7
+ const paths = ['bin', 'lib', 'tools', 'evaluate.mjs', 'server.mjs'];
8
+ const files = [];
9
+
10
+ function collect(path) {
11
+ if (path.endsWith('.mjs')) {
12
+ files.push(path);
13
+ return;
14
+ }
15
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
16
+ const child = join(path, entry.name);
17
+ if (entry.isDirectory()) collect(child);
18
+ else if (entry.name.endsWith('.mjs')) files.push(child);
19
+ }
20
+ }
21
+
22
+ for (const path of paths) collect(join(root, path));
23
+ for (const file of files.sort()) {
24
+ const result = spawnSync(process.execPath, ['--check', file], { stdio: 'inherit' });
25
+ if (result.status !== 0) process.exit(result.status || 1);
26
+ }
27
+ console.log(`Checked ${files.length} JavaScript files.`);
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { evaluateAdvisor } from '../lib/plan-advisor.mjs';
4
+ import { createJevAdvisor } from '../lib/jev-advisor.mjs';
5
+
6
+ // Separate explicit evaluation entry point; never invoked by the delivery gate.
7
+ const args = process.argv.slice(2);
8
+ if (!args[0] || args.some((arg, i) => i > 0 && arg !== '--jev')) {
9
+ console.error('Usage: node tools/evaluate-plan-advisor.mjs corpus.json [--jev]');
10
+ process.exitCode = 2;
11
+ } else {
12
+ try {
13
+ const cases = JSON.parse(await readFile(args[0], 'utf8'));
14
+ const advisor = args.includes('--jev') ? createJevAdvisor({ apiKey: process.env.TYPESAFE_API_KEY }) : undefined;
15
+ const baseline = await evaluateAdvisor(cases);
16
+ const shadow = advisor ? await evaluateAdvisor(cases, { advisor }) : null;
17
+ console.log(JSON.stringify({ version: 1, baseline, shadow, liveProviderEvaluated: Boolean(advisor), note: 'Explicit --jev transmits design data to TypeSafe. Cost remains unknown without injected pricing. Classifiers never authorize code.' }, null, 2));
18
+ if (shadow?.unavailable) process.exitCode = 1;
19
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
20
+ }