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,252 @@
1
+ import { test } from 'node:test';
2
+ import { fixtureDesign, fixtureReview } from './helpers/tested-design.mjs';
3
+ import assert from 'node:assert/strict';
4
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { spawn, spawnSync } from 'node:child_process';
9
+ import { domCheck } from '../lib/browser.mjs';
10
+
11
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
12
+ const serverPath = join(root, 'server.mjs');
13
+ const htmlPath = join(root, 'public', 'index.html');
14
+
15
+ async function serverFixture(t, envOverrides = {}) {
16
+ const cwd = mkdtempSync(join(tmpdir(), 'carthagent server '));
17
+ writeFileSync(join(cwd, 'app.mjs'), 'console.log("ok")\n');
18
+ const port = 32000 + Math.floor(Math.random() * 10000);
19
+ const token = 'test-token';
20
+ const child = spawn(process.execPath, [serverPath], {
21
+ cwd: root,
22
+ env: { ...process.env, CARTHAGENT_WORKSPACE: cwd, CARTHAGENT_PORT: String(port), CARTHAGENT_SERVER_TOKEN: token, ...envOverrides },
23
+ stdio: ['ignore', 'pipe', 'pipe']
24
+ });
25
+ t.after(() => { child.kill('SIGTERM'); rmSync(cwd, { recursive: true, force: true }); });
26
+ await new Promise((resolveReady, reject) => {
27
+ const timer = setTimeout(() => reject(new Error('server startup timeout')), 5000);
28
+ child.stdout.on('data', chunk => {
29
+ if (chunk.toString().includes('carthagent dashboard:')) { clearTimeout(timer); resolveReady(); }
30
+ });
31
+ child.on('error', error => { clearTimeout(timer); reject(error); });
32
+ child.on('exit', code => { if (code) { clearTimeout(timer); reject(new Error(`server exited ${code}`)); } });
33
+ });
34
+ return { cwd, token, url: `http://127.0.0.1:${port}` };
35
+ }
36
+
37
+ const json = (token, body) => ({
38
+ method: 'POST',
39
+ headers: { 'content-type': 'application/json', 'x-carthagent-token': token },
40
+ body: JSON.stringify(body)
41
+ });
42
+
43
+ async function approvedPost(server, path, body) {
44
+ if (path === 'checks/run') {
45
+ const state = await (await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } })).json();
46
+ const plan = { ...state.plan, design: fixtureDesign(state.plan) };
47
+ if (JSON.stringify(state.plan.design) !== JSON.stringify(plan.design)) {
48
+ const revised = await fetch(`${server.url}/api/plan/revise`, json(server.token, { reason: 'Fixture design', patch: { design: plan.design } }));
49
+ assert.equal(revised.status, 200, await revised.text());
50
+ }
51
+ let captureId;
52
+ for (const action of ['validate', 'inspect', 'review', 'approve']) {
53
+ const response = await fetch(`${server.url}/api/design`, json(server.token, { action, review: { ...fixtureReview(plan), captureId } }));
54
+ const body = await response.json();
55
+ assert.equal(response.status, 200, JSON.stringify(body));
56
+ if (action === 'inspect') captureId = body.result.id;
57
+ }
58
+ }
59
+ return fetch(`${server.url}/api/${path}`, json(server.token, body));
60
+ }
61
+
62
+ test('dashboard API requires its launch token and rejects foreign origins', async t => {
63
+ const server = await serverFixture(t);
64
+ assert.equal((await fetch(`${server.url}/api/status`)).status, 401);
65
+ const foreign = await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token, origin: 'https://example.com' } });
66
+ assert.equal(foreign.status, 403);
67
+ const response = await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } });
68
+ assert.equal(response.status, 200);
69
+ assert.match(response.headers.get('content-security-policy'), /frame-ancestors 'none'/);
70
+ assert.equal(response.headers.get('cache-control'), 'no-store');
71
+ const body = await response.json();
72
+ assert.equal(body.workspace, server.cwd);
73
+ assert.deepEqual(body.stepStatus, {});
74
+ });
75
+
76
+ test('dashboard status reports an available update from the release cache', async t => {
77
+ const dir = mkdtempSync(join(tmpdir(), 'carthagent update '));
78
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
79
+ // Seed the throttled cache so the check resolves without touching the network.
80
+ const cachePath = join(dir, 'update-check.json');
81
+ writeFileSync(cachePath, JSON.stringify({ checkedAt: Date.now(), release: { version: '99.0.0', tag: 'v99.0.0' } }));
82
+ const server = await serverFixture(t, { CARTHAGENT_UPDATE_CHECK_FILE: cachePath });
83
+ let body = {};
84
+ for (let i = 0; i < 50 && !body.update; i++) {
85
+ body = await (await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } })).json();
86
+ if (!body.update) await new Promise(r => setTimeout(r, 50));
87
+ }
88
+ assert.equal(body.update.latest, '99.0.0');
89
+ assert.equal(body.update.available, true);
90
+ assert.ok(body.update.installed);
91
+ });
92
+
93
+ test('dashboard omits update info when the check is disabled', async t => {
94
+ const server = await serverFixture(t, { CARTHAGENT_NO_UPDATE_CHECK: '1' });
95
+ const body = await (await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } })).json();
96
+ assert.equal(body.update, null);
97
+ });
98
+
99
+ test('dashboard persists plans and executes checks in the selected workspace', async t => {
100
+ const server = await serverFixture(t);
101
+ const plan = {
102
+ goal: 'Server fixture', assumptions: [], artifacts: ['.'], steps: ['Run'],
103
+ acceptance: [{ requirement: 'Runs', checks: ['smoke'] }],
104
+ checks: [{ id: 'smoke', kind: 'runtime', argv: [process.execPath, 'app.mjs'], timeoutSeconds: 10 }]
105
+ };
106
+ const saved = await fetch(`${server.url}/api/plan/set`, json(server.token, { plan }));
107
+ assert.equal(saved.status, 200, await saved.text());
108
+ const checked = await approvedPost(server, 'checks/run', { id: 'all' });
109
+ assert.equal(checked.status, 200, await checked.text());
110
+ const status = await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } });
111
+ const body = await status.json();
112
+ assert.equal(body.plan.goal, 'Server fixture');
113
+ assert.equal(body.evidence.smoke.passed, true);
114
+ assert.deepEqual(body.pendingChecks, []);
115
+ });
116
+
117
+ test('dashboard preserves command evidence but rejects new-requirement coverage until rerun and records reviews', async t => {
118
+ const server = await serverFixture(t);
119
+ const post = (path, body) => approvedPost(server, path, body);
120
+ const plan = { goal: 'Review API', artifacts: ['.'], steps: ['Run'], acceptance: [{ requirement: 'Runs', checks: ['smoke'] }], checks: [{ id: 'smoke', kind: 'runtime', argv: [process.execPath, 'app.mjs'], timeoutSeconds: 10 }] };
121
+ assert.equal((await post('plan/set', { plan })).status, 200);
122
+ assert.equal((await post('checks/run', { id: 'all' })).status, 200);
123
+ assert.equal((await post('plan/revise', { reason: 'New boundary', patch: { acceptance: [...plan.acceptance, { requirement: 'Boundary behavior', checks: ['smoke'] }] } })).status, 200);
124
+ const status = await (await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } })).json();
125
+ assert.deepEqual(status.pendingChecks, []);
126
+ assert.deepEqual(status.completionIssues.unverifiedRequirements, ['Boundary behavior']);
127
+ assert.equal((await post('finish', { status: 'verified' })).status, 400);
128
+ assert.equal((await post('checks/run', { id: 'all' })).status, 200);
129
+ const inspected = await post('review', { action: 'inspect' });
130
+ assert.equal(inspected.status, 200);
131
+ const { result } = await inspected.json();
132
+ assert.equal((await post('review', { action: 'record', captureId: result.id, coverage: ['Runs', 'Boundary behavior'].map(requirement => ({ requirement, assertions: 'Fixture command exits zero' })), probes: ['node app.mjs: ok'], findings: [], limitations: ['Fixture coverage only'] })).status, 200);
133
+ const finished = await post('finish', { status: 'verified' });
134
+ assert.equal(finished.status, 200);
135
+ const { state } = await finished.json();
136
+ assert.equal(state.handoff.reviewEvidence.id, result.id);
137
+ assert.equal(state.handoff.evidence.smoke.executedRevision, state.revision);
138
+ });
139
+
140
+ test('dashboard rejects plan replacement and finish during checks and releases after failure', async t => {
141
+ const server = await serverFixture(t);
142
+ mkdirSync(join(server.cwd, 'artifacts'));
143
+ const started = join(server.cwd, 'artifacts', 'started');
144
+ const release = join(server.cwd, 'artifacts', 'release');
145
+ const plan = {
146
+ goal: 'Concurrent café check', assumptions: [], artifacts: ['.'], steps: ['Run'],
147
+ acceptance: [{ requirement: 'Runs', checks: ['smoke'] }],
148
+ checks: [{ id: 'smoke', kind: 'runtime', argv: [process.execPath, '-e',
149
+ `const fs = require('node:fs'); fs.writeFileSync(${JSON.stringify(started)}, 'yes'); const timer = setInterval(() => { if (fs.existsSync(${JSON.stringify(release)})) { clearInterval(timer); process.exitCode = 1; } }, 20);`
150
+ ], timeoutSeconds: 5 }]
151
+ };
152
+ const post = (path, body) => approvedPost(server, path, body);
153
+ assert.equal((await post('plan/set', { plan })).status, 200);
154
+ const running = post('checks/run', { id: 'all' });
155
+ const deadline = Date.now() + 4000;
156
+ while (!existsSync(started) && Date.now() < deadline) await new Promise(r => setTimeout(r, 20));
157
+ assert.ok(existsSync(started), 'check subprocess reached barrier');
158
+ const competing = spawnSync(process.execPath, [join(root, 'bin/ctg.mjs'), 'check', 'all'], { cwd: server.cwd, encoding: 'utf8' });
159
+ assert.notEqual(competing.status, 0);
160
+ assert.match(competing.stderr, /Workspace busy/);
161
+ const replacement = { ...plan, goal: 'Replacement' };
162
+ assert.equal((await post('plan/set', { plan: replacement })).status, 409);
163
+ assert.equal((await post('finish', { status: 'blocked', limitations: ['Stop'] })).status, 409);
164
+ const status = await (await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } })).json();
165
+ assert.equal(status.plan.goal, plan.goal);
166
+ assert.deepEqual(status.evidence, {});
167
+ writeFileSync(release, 'go');
168
+ const result = await running;
169
+ assert.equal(result.status, 200);
170
+ assert.equal((await result.json()).results[0].passed, false);
171
+ assert.equal((await post('plan/set', { plan: replacement })).status, 200);
172
+ const after = await (await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } })).json();
173
+ assert.equal(after.plan.goal, 'Replacement');
174
+ assert.equal(after.evidence.smoke.passed, false, 'replacing an active plan preserves failure history');
175
+ assert.equal(after.revision, status.revision + 1);
176
+ });
177
+
178
+ test('dashboard revisions preserve evidence and enforce output and step completion', async t => {
179
+ const server = await serverFixture(t);
180
+ const post = (path, body) => approvedPost(server, path, body);
181
+ const status = async () => (await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } })).json();
182
+ const plan = {
183
+ goal: 'Migration report', assumptions: [], artifacts: ['app.mjs'], steps: ['Run'],
184
+ outputs: ['artifacts/report.json'],
185
+ acceptance: [{ requirement: 'Runs', checks: ['smoke'] }],
186
+ checks: [{ id: 'smoke', kind: 'runtime', argv: [process.execPath, 'app.mjs'], timeoutSeconds: 10 }],
187
+ };
188
+ assert.equal((await post('plan/set', { plan })).status, 200);
189
+ assert.equal((await post('checks/run', { id: 'all' })).status, 200);
190
+ const before = await status();
191
+ for (const patch of [null, {}, [], 'invalid', { evidence: {} }]) {
192
+ assert.equal((await post('plan/revise', { reason: 'Invalid patch', patch })).status, 400);
193
+ }
194
+ assert.equal((await status()).revision, before.revision);
195
+ assert.equal((await post('plan/revise', { reason: 'Discovered old callers', patch: { assumptions: ['Preserve compatibility'] } })).status, 200);
196
+ const after = await status();
197
+ assert.equal(after.runId, before.runId);
198
+ assert.equal(after.revision, before.revision + 1);
199
+ assert.deepEqual(after.pendingChecks, []);
200
+ assert.equal((await post('finish', { status: 'verified' })).status, 400);
201
+ assert.equal((await post('plan/revise', { reason: 'Try dropping report', patch: { outputs: [] } })).status, 400);
202
+ mkdirSync(join(server.cwd, 'artifacts'));
203
+ writeFileSync(join(server.cwd, 'artifacts/report.json'), '{}');
204
+ assert.equal((await post('finish', { status: 'verified' })).status, 200);
205
+ assert.equal((await post('plan/revise', { reason: 'Review a discovered caller', patch: { steps: [{ id: 'review', title: 'Review caller', checks: ['smoke'] }] } })).status, 200);
206
+ assert.deepEqual((await status()).completionIssues.incompleteSteps, ['review']);
207
+ assert.equal((await post('finish', { status: 'verified' })).status, 400);
208
+ assert.equal((await post('plan/revise', { reason: 'Try dropping requirement', patch: { acceptance: [{ requirement: 'Less work', checks: ['smoke'] }] } })).status, 400);
209
+ });
210
+
211
+ test('dashboard is self-contained and does not render API data with innerHTML', async () => {
212
+ const html = readFileSync(htmlPath, 'utf8');
213
+ assert.doesNotMatch(html, /https?:\/\/[^'"\s]+/);
214
+ assert.doesNotMatch(html, /\.innerHTML\s*=/);
215
+ assert.match(html, /textContent/);
216
+ // Active-step arrow effect is wired to live stepStatus.
217
+ assert.match(html, /@keyframes step-arrow/);
218
+ assert.match(html, /step-active/);
219
+ assert.match(html, /stepStatus/);
220
+ const rendered = await domCheck(html, [
221
+ { selector: '.shell' },
222
+ { selector: '#update-badge' },
223
+ { eval: 'document.getElementById("update-badge").hidden', expect: true },
224
+ { selector: '#planning-view' },
225
+ { eval: `(() => { const select = document.querySelector('#planning-view'); select.value = 'architecture'; select.dispatchEvent(new Event('change')); return document.querySelector('#d2').hidden; })()`, expect: true },
226
+ { eval: `(() => { const select = document.querySelector('#planning-view'); select.value = 'workflow'; select.dispatchEvent(new Event('change')); return document.querySelector('#planning-detail').textContent.includes('No active action recorded'); })()`, expect: true },
227
+ { selector: '.brand', text: /Carthagent/ },
228
+ { eval: 'document.documentElement.dataset.theme', expect: 'carthage' },
229
+ { eval: `(() => { const option = document.querySelector('[data-theme-value="paper"]'); option.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); return document.documentElement.dataset.theme; })()`, expect: 'paper' },
230
+ { action: 'click', selector: '[data-theme-value="carthage"]', then: { eval: 'localStorage.getItem("carthagent-theme")', expect: 'carthage' } },
231
+ { console: 'error-free' },
232
+ ]);
233
+ assert.equal(rendered.pass, true, rendered.failures.join('\n'));
234
+ });
235
+
236
+
237
+ test('raw HTTP and CLI cannot run arbitrary checks before tested-plan approval', async t => {
238
+ const server = await serverFixture(t);
239
+ const plan = { goal: 'Gate', artifacts: ['.'], steps: ['Run'], acceptance: [{ requirement: 'Runs', checks: ['smoke'] }], checks: [{ id: 'smoke', kind: 'runtime', argv: [process.execPath, '-e', 'require("node:fs").writeFileSync("bypass.txt", "bad")'], timeoutSeconds: 10 }] };
240
+ const saved = await fetch(`${server.url}/api/plan/set`, json(server.token, { plan }));
241
+ assert.equal(saved.status, 200);
242
+ assert.equal(existsSync(join(server.cwd, '.harness', 'workspace.lock')), false, 'a completed synchronous mutation releases its lease before its response is consumed');
243
+ const blocked = await fetch(`${server.url}/api/checks/run`, json(server.token, { id: 'all' }));
244
+ assert.notEqual(blocked.status, 200);
245
+ const blockedBody = await blocked.text();
246
+ assert.match(blockedBody, /Implementation locked/);
247
+ assert.equal(existsSync(join(server.cwd, '.harness', 'workspace.lock')), false, 'a rejected mutation also releases its lease before its response is consumed');
248
+ const cli = spawnSync(process.execPath, [join(root, 'bin/ctg.mjs'), 'check', 'all'], { cwd: server.cwd, encoding: 'utf8' });
249
+ assert.notEqual(cli.status, 0);
250
+ assert.match(cli.stderr, /Implementation locked/);
251
+ assert.equal(existsSync(join(server.cwd, 'bypass.txt')), false);
252
+ });
@@ -0,0 +1,77 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { createShipAdvisor, createShipJev } from '../lib/ship-jev.mjs';
7
+ import { resolveShipAccessToken } from '../lib/ship-auth.mjs';
8
+ import { CARTHAGENT_SHIP_PROVIDER_ID } from '../lib/providers/names.mjs';
9
+
10
+ const plan = { goal: 'g', acceptance: [], checks: [], outputs: [], assumptions: [], design: 'd', workflow: 'w' };
11
+ const jevPayload = model => JSON.stringify({
12
+ model, answers: { srp: { type: 'noul', noul: 0.62 }, di: { type: 'noul', noul: 0.1 }, tests: { type: 'noul', noul: 0.3 } },
13
+ usage: { input_tokens: 120, output_tokens: 9, cost: 0 },
14
+ });
15
+
16
+ function agentDir(t, credential) {
17
+ const dir = mkdtempSync(join(tmpdir(), 'ctg-ship-auth-'));
18
+ writeFileSync(join(dir, 'auth.json'), JSON.stringify({ [CARTHAGENT_SHIP_PROVIDER_ID]: credential }));
19
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
20
+ return dir;
21
+ }
22
+
23
+ test('Ship Jev posts the plan state to the managed /jev endpoint', async () => {
24
+ const calls = [];
25
+ const fetchImpl = async (url, init) => {
26
+ calls.push({ url, init });
27
+ return new Response(jevPayload('jev-latest'), { status: 200, headers: { 'content-type': 'application/json' } });
28
+ };
29
+ const advisor = createShipJev({ accessToken: 'tok-1', baseUrl: 'https://api.carthagent.xyz/v1', fetchImpl });
30
+ const result = await advisor.classify(plan);
31
+ assert.equal(calls.length, 1);
32
+ assert.equal(calls[0].url, 'https://api.carthagent.xyz/v1/jev');
33
+ assert.equal(calls[0].init.headers.authorization, 'Bearer tok-1');
34
+ const body = JSON.parse(calls[0].init.body);
35
+ assert.deepEqual(body.state, plan);
36
+ assert.equal(body.model, undefined); // the backend owns the managed model choice
37
+ assert.equal(result.probabilities.srp, 0.62);
38
+ assert.equal(result.costUsd, 0);
39
+ });
40
+
41
+ test('resolveShipAccessToken reuses a fresh credential and refreshes an expired one', async t => {
42
+ const fresh = agentDir(t, { type: 'oauth', access: 'live-tok', refresh: 'rt', expires: Date.now() + 3_600_000 });
43
+ assert.equal(await resolveShipAccessToken({ agentDir: fresh }), 'live-tok');
44
+
45
+ const stale = agentDir(t, { type: 'oauth', access: 'dead-tok', refresh: 'rt-1', expires: Date.now() - 1000 });
46
+ const fetchImpl = async (url, init) => {
47
+ const body = JSON.parse(init.body);
48
+ assert.equal(url, 'https://api.carthagent.xyz/v1/token/refresh');
49
+ assert.equal(body.refreshToken, 'rt-1');
50
+ return new Response(JSON.stringify({ accessToken: 'new-tok', refreshToken: 'rt-2', expiresIn: 3600, tokenType: 'Bearer' }), { status: 200 });
51
+ };
52
+ assert.equal(await resolveShipAccessToken({ agentDir: stale, fetchImpl }), 'new-tok');
53
+ const stored = JSON.parse(readFileSync(join(stale, 'auth.json'), 'utf8'))[CARTHAGENT_SHIP_PROVIDER_ID];
54
+ assert.equal(stored.access, 'new-tok');
55
+ assert.equal(stored.refresh, 'rt-2');
56
+
57
+ const empty = mkdtempSync(join(tmpdir(), 'ctg-ship-auth-'));
58
+ t.after(() => rmSync(empty, { recursive: true, force: true }));
59
+ assert.equal(await resolveShipAccessToken({ agentDir: empty }), null);
60
+ });
61
+
62
+ test('Ship advisor resolves the stored credential lazily and stays bounded', async t => {
63
+ const dir = agentDir(t, { type: 'oauth', access: 'tok-stored', refresh: 'rt', expires: Date.now() + 3_600_000 });
64
+ const calls = [];
65
+ const fetchImpl = async url => {
66
+ calls.push(url);
67
+ return new Response(jevPayload('typesafe/jev-1.13.4'), { status: 200 });
68
+ };
69
+ const advisor = createShipAdvisor({ agentDir: dir, fetchImpl });
70
+ const result = await advisor.classify(plan);
71
+ assert.equal(result.probabilities.tests, 0.3);
72
+ assert.equal(calls.length, 1);
73
+ assert.match(calls[0], /\/v1\/jev$/);
74
+
75
+ const missing = createShipAdvisor({ agentDir: mkdtempSync(join(tmpdir(), 'ctg-ship-auth-')), fetchImpl });
76
+ await assert.rejects(() => missing.classify(plan), /Ship credential/);
77
+ });