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,385 @@
1
+ #!/usr/bin/env node
2
+ /** Offline real-host test. Uses a disposable HOME and never discovers the user's service. */
3
+ import assert from 'node:assert/strict';
4
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
5
+ import { join, resolve } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { execFile, spawn } from 'node:child_process';
8
+ import { createServer } from 'node:http';
9
+ import { once } from 'node:events';
10
+ import { randomBytes } from 'node:crypto';
11
+ import { promisify } from 'node:util';
12
+ import { OpenCode } from '@opencode/client';
13
+ import { resolveOpenCode } from '../lib/opencode/runtime.mjs';
14
+ import { cancelSession } from '../lib/opencode/service.mjs';
15
+ import { runHeadless } from '../lib/opencode/headless.mjs';
16
+
17
+ const root = resolve(import.meta.dirname, '..');
18
+ const plugin = resolve(process.argv[2] ?? join(root, 'plugins/carthagent'));
19
+ const archiveFixture = resolve(root, 'tests/fixtures/archive-plugin');
20
+ const binary = await resolveOpenCode();
21
+ const execFileAsync = promisify(execFile);
22
+ const temp = await mkdtemp(join(tmpdir(), 'ctg-host-smoke-'));
23
+ const password = randomBytes(24).toString('hex');
24
+ const providerKey = `sentinel-${randomBytes(24).toString('hex')}`;
25
+ let child;
26
+ let childClosed = false;
27
+ let output = '';
28
+ let deviceAuthorizationBody;
29
+ const gatewayAuth = [];
30
+ const gatewayServer = createServer((request, response) => {
31
+ gatewayAuth.push({ authorization: request.headers.authorization, url: request.url });
32
+ response.writeHead(200, { 'content-type': 'application/json' });
33
+ response.end(JSON.stringify(request.url?.endsWith('/models')
34
+ ? { data: [{ id: 'sentinel-model' }] }
35
+ : { id: 'chatcmpl-sentinel', object: 'chat.completion', choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }));
36
+ });
37
+ await new Promise((resolveListen, reject) => gatewayServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
38
+ const gatewayAddress = gatewayServer.address();
39
+ const authServer = createServer((request, response) => {
40
+ let body = '';
41
+ request.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
42
+ request.on('end', () => {
43
+ if (request.url === '/v1/device/authorizations') {
44
+ deviceAuthorizationBody = JSON.parse(body);
45
+ response.writeHead(201, { 'content-type': 'application/json' });
46
+ response.end(JSON.stringify({ deviceCode: 'offline-device-code', userCode: 'OFFLINE', verificationUri: 'http://127.0.0.1/verify', expiresIn: 120, interval: 1 }));
47
+ } else if (request.url === '/v1/device/token') {
48
+ response.writeHead(400, { 'content-type': 'application/json' });
49
+ response.end(JSON.stringify({ error: 'authorization_pending' }));
50
+ } else {
51
+ response.writeHead(404).end();
52
+ }
53
+ });
54
+ });
55
+ await new Promise((resolveListen, reject) => authServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
56
+ const authAddress = authServer.address();
57
+ let fixtureRequests = 0;
58
+ const fixture = createServer(async (req, res) => {
59
+ fixtureRequests += 1;
60
+ let raw = '';
61
+ for await (const chunk of req) raw += chunk;
62
+ let body;
63
+ try { body = JSON.parse(raw || '{}'); } catch { body = {}; }
64
+ const currentUser = [...(body.messages ?? [])].reverse().find((message) => message.role === 'user');
65
+ const promptText = JSON.stringify(currentUser);
66
+ if (promptText.includes('WRAPPER_TOOL') && body.tools?.some((tool) => tool.function?.name === 'read') && !(body.messages ?? []).some((message) => message.role === 'tool')) {
67
+ const chunk = (delta, finish_reason = null) => `data: ${JSON.stringify({
68
+ id: 'chatcmpl-ctg-tool', object: 'chat.completion.chunk', created: 1, model: 'fixture',
69
+ choices: [{ index: 0, delta, finish_reason }],
70
+ })}\n\n`;
71
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
72
+ res.end(`${chunk({ role: 'assistant', tool_calls: [{ index: 0, id: 'wrapper-read', type: 'function',
73
+ function: { name: 'read', arguments: JSON.stringify({ path: join(temp, 'project/wrapper-tool.txt') }) } }] })}${chunk({}, 'tool_calls')}data: [DONE]\n\n`);
74
+ return;
75
+ }
76
+ if (promptText.includes('WRAPPER_FAILURE')) {
77
+ res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: { message: 'Offline wrapper rejection', type: 'invalid_request_error' } }));
78
+ return;
79
+ }
80
+ if (req.method !== 'POST' || req.url !== '/v1/chat/completions') {
81
+ res.writeHead(404).end();
82
+ return;
83
+ }
84
+ res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
85
+ const created = Math.floor(Date.now() / 1000);
86
+ const chunk = (delta, finish_reason = null) => `data: ${JSON.stringify({
87
+ id: 'chatcmpl-ctg-fixture', object: 'chat.completion.chunk', created, model: 'fixture',
88
+ choices: [{ index: 0, delta, finish_reason }],
89
+ })}\n\n`;
90
+ const reply = promptText.includes('WRAPPER_TOOL') ? 'FINAL_AFTER_TOOL' : promptText.includes('WRAPPER_SECOND') ? 'SECOND_TURN' : 'CTG_FIXTURE_OK';
91
+ res.write(chunk({ role: 'assistant', content: reply }));
92
+ res.write(chunk({}, 'stop'));
93
+ res.end('data: [DONE]\n\n');
94
+ });
95
+ await new Promise((resolveListen, reject) => fixture.once('error', reject).listen(0, '127.0.0.1', resolveListen));
96
+ try {
97
+ const cwd = join(temp, 'project');
98
+ const config = join(temp, 'config/opencode');
99
+ const homeConfig = join(temp, 'home/.config/opencode');
100
+ const legacy = join(temp, 'home/.carthagent/agent/sessions');
101
+ await Promise.all([
102
+ mkdir(cwd), mkdir(config, { recursive: true }), mkdir(homeConfig, { recursive: true }), mkdir(legacy, { recursive: true }),
103
+ mkdir(join(temp, 'appdata'), { recursive: true }), mkdir(join(temp, 'localappdata'), { recursive: true }),
104
+ ]);
105
+ await writeFile(join(homeConfig, 'opencode.json'), JSON.stringify({ model: 'home-poison/model', plugins: ['home-poison-plugin'] }));
106
+ await writeFile(join(cwd, 'opencode.json'), JSON.stringify({ model: 'project-poison/model', plugins: ['project-poison-plugin'] }));
107
+ await writeFile(join(legacy, 'legacy.jsonl'), '{"type":"session","id":"legacy-fixture"}\n{"type":"message","message":{"role":"user","content":"fixture needle"}}\nmalformed\n');
108
+ await writeFile(join(config, 'opencode.json'), JSON.stringify({
109
+ plugins: [plugin, archiveFixture], update: 'disable',
110
+ model: 'ctgfixture/fixture',
111
+ providers: {
112
+ ctgfixture: {
113
+ name: 'CTG local prompt fixture',
114
+ package: '@opencode/ai/providers/openai-compatible',
115
+ settings: { baseURL: `http://127.0.0.1:${fixture.address().port}/v1` },
116
+ models: { fixture: { name: 'Fixture', limit: { context: 8192, output: 1024 } } },
117
+ },
118
+ },
119
+ }));
120
+ const env = Object.fromEntries(['PATH', 'SystemRoot', 'WINDIR', 'ComSpec', 'PATHEXT', 'USERPROFILE']
121
+ .filter((key) => process.env[key])
122
+ .map((key) => [key, process.env[key]]));
123
+ Object.assign(env, {
124
+ HOME: join(temp, 'home'),
125
+ USERPROFILE: join(temp, 'home'),
126
+ APPDATA: join(temp, 'appdata'),
127
+ LOCALAPPDATA: join(temp, 'localappdata'),
128
+ XDG_CONFIG_HOME: join(temp, 'config'),
129
+ XDG_DATA_HOME: join(temp, 'data'),
130
+ XDG_STATE_HOME: join(temp, 'state'),
131
+ XDG_CACHE_HOME: join(temp, 'cache'),
132
+ OPENCODE_CONFIG_DIR: config,
133
+ OPENCODE_CONFIG: join(config, 'opencode.json'),
134
+ OPENCODE_CONFIG_PROJECT_DISABLE: 'true',
135
+ OPENCODE_DB: join(temp, 'database.sqlite'),
136
+ OPENCODE_SERVER_PASSWORD: password,
137
+ CARTHAGENT_CLOUD_URL: `http://127.0.0.1:${authAddress.port}`,
138
+ EXPLABS_API_KEY: providerKey,
139
+ EXP_GATEWAY_URL: `http://127.0.0.1:${gatewayAddress.port}`,
140
+ });
141
+ child = spawn(binary, [
142
+ 'serve', '--hostname', '127.0.0.1', '--port', '0', '--print-logs', '--log-level', 'debug',
143
+ ], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
144
+ child.once('close', () => { childClosed = true; });
145
+ const endpoint = await new Promise((resolveEndpoint, reject) => {
146
+ const timer = setTimeout(() => reject(new Error(`Host startup timed out: ${output}`)), 30_000);
147
+ const collect = (chunk) => {
148
+ output = (output + chunk.toString()).slice(-16_384);
149
+ const address = output.match(/http:\/\/127\.0\.0\.1:\d+/)?.[0];
150
+ if (address) { clearTimeout(timer); resolveEndpoint(address); }
151
+ };
152
+ child.stdout.on('data', collect);
153
+ child.stderr.on('data', collect);
154
+ child.once('error', (error) => { clearTimeout(timer); reject(error); });
155
+ child.once('exit', (code) => { clearTimeout(timer); reject(new Error(`Host exited ${code}: ${output}`)); });
156
+ });
157
+ const client = OpenCode.make({
158
+ baseUrl: endpoint,
159
+ headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
160
+ });
161
+ const request = { signal: AbortSignal.timeout(90_000) };
162
+ const info = await client.server.info(request);
163
+ assert.equal(info.version, '2.0.32');
164
+ const isolationFetch = async (url, init) => {
165
+ const response = await fetch(url, init);
166
+ if (new URL(url).pathname === '/api/config') {
167
+ const entries = await response.clone().json();
168
+ assert.ok(!entries.some((entry) => entry.type === 'document' && [join(homeConfig, 'opencode.json'), join(cwd, 'opencode.json')].includes(entry.path)), `home and workspace config must not be discovered: ${JSON.stringify(entries)}`);
169
+ assert.ok(!JSON.stringify(entries).includes('poison'), `foreign config/plugin discovery must not leak into the CTG profile: ${JSON.stringify(entries)}`);
170
+ }
171
+ return response;
172
+ };
173
+ const isolatedClient = OpenCode.make({
174
+ baseUrl: endpoint,
175
+ headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
176
+ fetch: isolationFetch,
177
+ });
178
+ const configEntries = await isolatedClient.config.get({ location: { directory: cwd } }, request);
179
+ const merged = configEntries.find((entry) => entry.type === 'document')?.info;
180
+ assert.deepEqual(merged.model, { providerID: 'ctgfixture', model: 'fixture' }, 'local model config must be loaded');
181
+ assert.ok(merged.providers?.ctgfixture, 'custom fixture provider config must be loaded');
182
+ const unauthorized = await fetch(`${endpoint}/api/info`, { signal: request.signal });
183
+ assert.equal(unauthorized.status, 401);
184
+ const session = await client.session.create({
185
+ location: { directory: cwd }, title: 'CTG host smoke',
186
+ model: { providerID: 'ctgfixture', id: 'fixture' },
187
+ }, request);
188
+ let plugins;
189
+ const deadline = Date.now() + 15_000;
190
+ do {
191
+ plugins = await client.plugin.list({ location: { directory: cwd } }, request);
192
+ if (plugins.data.some((entry) => entry.id === 'carthagent')) break;
193
+ await new Promise((done) => setTimeout(done, 200));
194
+ } while (Date.now() < deadline);
195
+ const nativePlugin = plugins.data.find((entry) => entry.id === 'carthagent');
196
+ const archivePlugin = plugins.data.find((entry) => entry.id === 'ctg-archive-fixture');
197
+ assert.ok(nativePlugin, `Carthagent plugin must become visible: ${JSON.stringify(plugins.data)}`);
198
+ assert.equal(nativePlugin.state.status, 'active');
199
+ assert.equal(archivePlugin?.state.status, 'active', 'archive registration fixture must become visible');
200
+ const providers = await client.provider.list({ location: { directory: cwd } }, request);
201
+ assert.ok(providers.data.some((provider) => provider.id === 'experiential-labs'),
202
+ 'plugin setup must register the Ship provider with the real host');
203
+ const integrations = await client.integration.list({ location: { directory: cwd } }, request);
204
+ const ship = integrations.data.find(({ id }) => id === 'experiential-labs');
205
+ assert.ok(ship, 'the native provider catalog must supply a real Ship integration record');
206
+ assert.ok(ship.methods.some(({ type, id }) => type === 'oauth' && id === 'carthagent-device'),
207
+ 'plugin integration transform must register the Ship device-OAuth method');
208
+ assert.ok(ship.methods.some(({ type }) => type === 'key'),
209
+ 'plugin integration transform must register the Ship API-key method');
210
+ assert.ok(ship.methods.some(({ type, names }) => type === 'env' && names?.includes('EXPLABS_API_KEY')),
211
+ 'the native integration catalog resolves BYOK from the named environment credential');
212
+ const models = await client.model.list({ location: { directory: cwd } }, request);
213
+ const serializedInventory = JSON.stringify({ providers: providers.data, models: models.data, integrations: integrations.data });
214
+ assert.equal(serializedInventory.includes(providerKey), false, 'provider and integration inventories must not expose the environment secret');
215
+ assert.equal(output.includes(providerKey), false, 'OpenCode/plugin logs must not expose the environment secret');
216
+ assert.ok(models.data.some(model => model.providerID === 'experiential-labs' && model.id === 'sentinel-model'),
217
+ 'environment-backed Ship model must be available through native model inventory');
218
+ const inferenceSession = await client.session.create({
219
+ location: { directory: cwd },
220
+ title: 'CTG BYOK auth fixture',
221
+ model: { id: 'sentinel-model', providerID: 'experiential-labs' },
222
+ }, request);
223
+ await client.session.prompt({ sessionID: inferenceSession.id, text: 'Reply with ok.' }, request);
224
+ const inferenceDeadline = Date.now() + 10_000;
225
+ while (!gatewayAuth.some(({ authorization, url }) => authorization === `Bearer ${providerKey}` && url.endsWith('/chat/completions')) && Date.now() < inferenceDeadline) {
226
+ await new Promise(done => setTimeout(done, 50));
227
+ }
228
+ assert.ok(gatewayAuth.some(({ authorization, url }) => authorization === `Bearer ${providerKey}` && url.endsWith('/chat/completions')),
229
+ 'the native-resolved key must authenticate an actual provider inference request');
230
+ console.log(JSON.stringify({ providers: providers.data.map((provider) => provider.id), shipMethods: ship.methods.map(({ id, type, names }) => ({ id, type, names })) }));
231
+ const health = await client.rpc.call({
232
+ rpcID: 'carthagent', method: 'health', input: {}, location: { directory: cwd },
233
+ }, request);
234
+ console.log(JSON.stringify({ version: info.version, plugin: nativePlugin, health }, null, 2));
235
+ assert.equal(health.output.directory, cwd);
236
+ assert.equal(health.output.protocol, 1);
237
+ const auth = await client.rpc.call({
238
+ rpcID: 'carthagent', method: 'authStart', input: {}, location: { directory: cwd },
239
+ }, request);
240
+ assert.equal(auth.output.userCode, 'OFFLINE');
241
+ assert.match(auth.output.flowId, /^[0-9a-f-]{36}$/);
242
+ assert.match(deviceAuthorizationBody.installId, /^[0-9a-f-]{36}$/);
243
+ assert.equal((await client.rpc.call({
244
+ rpcID: 'carthagent', method: 'authCancel', input: { flowId: auth.output.flowId }, location: { directory: cwd },
245
+ }, request)).output.cancelled, true);
246
+ console.log('Device authorization start/cancel RPC passed against offline OpenCode host.');
247
+ const echo = await client.rpc.call({
248
+ rpcID: 'carthagent', method: 'echo', input: { text: 'native RPC fixture' }, location: { directory: cwd },
249
+ }, request);
250
+ assert.deepEqual(echo.output, { text: 'native RPC fixture' });
251
+ const ensuredService = {
252
+ url: endpoint,
253
+ headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
254
+ info: { password },
255
+ };
256
+ const wrappedRun = await runHeadless(ensuredService, cwd, 'Reply with the fixture response.', { sessionID: session.id, timeout: 90_000 });
257
+ const assistant = wrappedRun.result?.message;
258
+ assert.equal(wrappedRun.outcome, 'succeeded');
259
+ assert.equal(wrappedRun.terminal, true, 'native wrapper must return a projected terminal session outcome');
260
+ assert.equal(wrappedRun.budgetReason, undefined, 'a successful terminal turn must not carry a budget failure reason');
261
+ assert.ok(assistant, 'headless prompt must produce an assistant response');
262
+ assert.equal(assistant.finish, 'stop');
263
+ assert.equal(assistant.content.filter((part) => part.type === 'text').map((part) => part.text).join(''), 'CTG_FIXTURE_OK');
264
+ assert.equal(wrappedRun.accepted.type, 'user');
265
+ assert.ok(wrappedRun.messages.length >= 2);
266
+ assert.ok(fixtureRequests > 0, 'assistant prompt must reach the local fixture endpoint');
267
+ const secondRun = await runHeadless(ensuredService, cwd, 'WRAPPER_SECOND', { sessionID: session.id, timeout: 90_000 });
268
+ assert.equal(secondRun.outcome, 'succeeded');
269
+ assert.equal(secondRun.result.text, 'SECOND_TURN', 'resumed wrapper calls must select the current turn output');
270
+ assert.equal(secondRun.budgetReason, undefined, 'a successful resumed turn must not carry a budget failure reason');
271
+ let failedRun;
272
+ try { failedRun = await runHeadless(ensuredService, cwd, 'WRAPPER_FAILURE', { sessionID: session.id, timeout: 90_000 }); }
273
+ catch (error) { failedRun = { error }; }
274
+ assert.ok(failedRun.error || failedRun.outcome === 'failed' || failedRun.session?.outcome === 'failed', `provider failure should be exposed, not hidden by prior output: ${JSON.stringify(failedRun)}`);
275
+ const recoveredRun = await runHeadless(ensuredService, cwd, 'WRAPPER_SECOND', { sessionID: session.id, timeout: 90_000 });
276
+ assert.equal(recoveredRun.outcome, 'succeeded');
277
+ assert.equal(recoveredRun.result.text, 'SECOND_TURN');
278
+ await writeFile(join(cwd, 'wrapper-tool.txt'), 'Read this through the native tool.\n');
279
+ const toolRun = await runHeadless(ensuredService, cwd, 'WRAPPER_TOOL', { timeout: 90_000 });
280
+ assert.equal(toolRun.outcome, 'succeeded');
281
+ assert.equal(toolRun.result.text, 'FINAL_AFTER_TOOL', 'must return the final assistant response after the read tool, not its tool-call message');
282
+ const unavailableRun = await runHeadless(ensuredService, cwd, 'UNAVAILABLE_MODEL', {
283
+ timeout: 30_000,
284
+ session: { model: { providerID: 'absent-fixture', id: 'missing' } },
285
+ });
286
+ assert.equal(unavailableRun.outcome, 'failed', `missing provider must be exposed as a terminal failure: ${JSON.stringify(unavailableRun)}`);
287
+ assert.equal(unavailableRun.terminal, true);
288
+ assert.ok(unavailableRun.error || unavailableRun.session.error, 'missing provider failure should preserve a useful error');
289
+ const idleInterrupt = await cancelSession(ensuredService, session.id, { location: { directory: cwd }, timeout: 5000 });
290
+ assert.equal(idleInterrupt.interrupted, false, 'wrapper cancellation should use the v2 bounded interrupt API and report idle state');
291
+ const requestsBeforeAttach = fixtureRequests;
292
+ const attachedRun = await new Promise((resolveRun, reject) => {
293
+ const attached = spawn(binary, [
294
+ 'run', '--server', endpoint, '--session', session.id, '--format', 'json',
295
+ 'Reply with the fixture response.',
296
+ ], { cwd, env: { ...env, OPENCODE_PASSWORD: password }, stdio: ['ignore', 'pipe', 'pipe'] });
297
+ let stdout = '';
298
+ let stderr = '';
299
+ const timer = setTimeout(() => attached.kill('SIGKILL'), 90_000);
300
+ attached.stdout.setEncoding('utf8').on('data', (chunk) => { stdout += chunk; });
301
+ attached.stderr.setEncoding('utf8').on('data', (chunk) => { stderr += chunk; });
302
+ attached.once('error', (error) => { clearTimeout(timer); reject(error); });
303
+ attached.once('close', (code, signal) => {
304
+ clearTimeout(timer);
305
+ if (code === 0) resolveRun(stdout);
306
+ else reject(new Error(`Attached native CLI exited ${code ?? signal}: ${stderr}`));
307
+ });
308
+ });
309
+ assert.match(attachedRun, /CTG_FIXTURE_OK/, 'the packaged CLI should attach to and prompt the ensured service');
310
+ assert.ok(fixtureRequests > requestsBeforeAttach, 'the attached CLI request must be handled by the already-running fixture-backed service');
311
+ const pty = await client.pty.create({
312
+ location: { directory: cwd },
313
+ command: binary,
314
+ args: ['--server', endpoint, cwd],
315
+ cwd,
316
+ title: 'CTG native TUI attach smoke',
317
+ env: { ...env, OPENCODE_PASSWORD: password },
318
+ }, request);
319
+ const ptyID = pty.data.id;
320
+ assert.ok(ptyID, 'the attached service should create a native PTY');
321
+ let ptyState;
322
+ const ptyDeadline = Date.now() + 10_000;
323
+ do {
324
+ ptyState = await client.pty.get({ ptyID, location: { directory: cwd } }, request);
325
+ if (ptyState.data.status === 'running') break;
326
+ await new Promise((done) => setTimeout(done, 100));
327
+ } while (Date.now() < ptyDeadline);
328
+ assert.equal(ptyState.data.status, 'running', 'native TUI should attach and remain active in the service-owned PTY');
329
+ await client.pty.remove({ ptyID, location: { directory: cwd } }, request);
330
+ await assert.rejects(client.rpc.call({
331
+ rpcID: 'carthagent', method: 'health', input: { unexpected: true }, location: { directory: cwd },
332
+ }, request));
333
+ const archiveCall = (method, input) => client.rpc.call({
334
+ rpcID: 'carthagent.archive', method, input, location: { directory: cwd },
335
+ }, request);
336
+ const listed = await archiveCall('list', {});
337
+ assert.equal(listed.output.total, 1);
338
+ const archiveID = listed.output.items[0].id;
339
+ assert.match(archiveID, /^[a-f0-9]{64}$/);
340
+ assert.equal('path' in listed.output.items[0], false);
341
+ const searched = await archiveCall('search', { query: 'needle' });
342
+ assert.equal(searched.output.items[0].id, archiveID);
343
+ const transcript = await archiveCall('read', { id: archiveID });
344
+ assert.equal(transcript.output.archiveOnly, true);
345
+ assert.equal(transcript.output.records.length, 2);
346
+ assert.deepEqual(transcript.output.malformedLines, [{ line: 3 }]);
347
+ const exported = await archiveCall('export', { id: archiveID, format: 'jsonl' });
348
+ assert.match(exported.output.content, /legacy-fixture/);
349
+ await assert.rejects(archiveCall('read', { id: join(temp, 'outside.jsonl') }), (error) => error.type === 'archive_error');
350
+ await assert.rejects(archiveCall('search', { query: ' ' }), (error) => error.type === 'archive_error');
351
+ await assert.rejects(archiveCall('list', { limit: 101 }), (error) => error.type === 'rpc.invalid_input');
352
+ assert.equal(await readFile(join(legacy, 'legacy.jsonl'), 'utf8'), '{"type":"session","id":"legacy-fixture"}\n{"type":"message","message":{"role":"user","content":"fixture needle"}}\nmalformed\n');
353
+ console.log('Native plugin, archive RPC methods, output schemas, and declared failures passed against OpenCode 2.0.32.');
354
+ } catch (error) {
355
+ console.error(JSON.stringify({ fixtureRequests }));
356
+ console.error(output.replaceAll(password, '[redacted]'));
357
+ throw error;
358
+ } finally {
359
+ if (child) {
360
+ const closed = childClosed ? Promise.resolve() : once(child, 'close').then(() => { childClosed = true; });
361
+ if (child.exitCode === null && child.signalCode === null) {
362
+ if (process.platform === 'win32') {
363
+ // This PID is the exact host process spawned above; /T is needed to
364
+ // close any descendants that can keep the temporary workspace locked.
365
+ await execFileAsync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F']).catch(() => {});
366
+ } else {
367
+ child.kill('SIGTERM');
368
+ const timer = setTimeout(() => {
369
+ if (!childClosed && child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
370
+ }, 5_000);
371
+ await closed;
372
+ clearTimeout(timer);
373
+ }
374
+ if (!childClosed) await closed;
375
+ } else {
376
+ await closed;
377
+ }
378
+ }
379
+ await Promise.all([
380
+ new Promise((resolveClose) => authServer.close(resolveClose)),
381
+ new Promise((resolveClose) => gatewayServer.close(resolveClose)),
382
+ new Promise((resolveClose) => fixture.close(resolveClose)),
383
+ ]);
384
+ await rm(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 250 });
385
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /** Native failure/cancellation/concurrent-session acceptance without provider spend. */
3
+ import assert from 'node:assert/strict';
4
+ import { nativeFixture, streamText } from './native-fixture.mjs';
5
+
6
+ let admitted;
7
+ const pending = new Promise(resolve => { admitted = resolve; });
8
+ let disconnected;
9
+ const cancelled = new Promise(resolve => { disconnected = resolve; });
10
+ const host = await nativeFixture({
11
+ plugin: process.argv[2],
12
+ respond(body, res) {
13
+ // Match the exact user message so title generation stays independent.
14
+ const last = body.messages.at(-1);
15
+ if (last?.role === 'user' && last.content === 'CTG_FIXTURE_FAILURE') {
16
+ res.writeHead(400, { 'content-type': 'application/json' });
17
+ res.end(JSON.stringify({ error: { message: 'Deterministic provider rejection', type: 'invalid_request_error' } }));
18
+ } else if (last?.role === 'user' && last.content === 'CTG_FIXTURE_CANCEL') {
19
+ res.once('close', disconnected);
20
+ admitted();
21
+ } else streamText(res);
22
+ },
23
+ });
24
+
25
+ async function bounded(promise, message) {
26
+ let timer;
27
+ try {
28
+ return await Promise.race([promise, new Promise((_, reject) => {
29
+ timer = setTimeout(() => reject(Error(message)), 10_000);
30
+ })]);
31
+ } finally { clearTimeout(timer); }
32
+ }
33
+
34
+ try {
35
+ const { client, location, model, request } = host;
36
+ await host.active('carthagent', location);
37
+ const create = title => client.session.create({ location, title, model,
38
+ metadata: { 'carthagent.options': { guide: false } } }, request());
39
+ const [failed, interrupted, healthy] = await Promise.all(['Failure', 'Cancellation', 'Healthy'].map(create));
40
+ await client.session.prompt({ sessionID: failed.id, text: 'CTG_FIXTURE_FAILURE' }, request());
41
+ assert.equal(await host.outcome(failed.id), 'failed');
42
+ const failures = await client.session.context({ sessionID: failed.id }, request());
43
+ assert.equal(failures.filter(message => message.type === 'assistant').at(-1).finish, 'error');
44
+ assert.equal(failures.at(-1).outcome, 'failed');
45
+
46
+ await client.session.prompt({ sessionID: interrupted.id, text: 'CTG_FIXTURE_CANCEL' }, request());
47
+ await bounded(pending, 'Cancellable provider request was never issued');
48
+ const queued = await client.session.prompt({ sessionID: interrupted.id, text: 'CTG_FIXTURE_QUEUED', delivery: 'queue', resume: false }, request());
49
+ await client.session.prompt({ sessionID: healthy.id, text: 'Independent healthy request' }, request());
50
+ const started = performance.now();
51
+ await client.session.interrupt({ sessionID: interrupted.id, resume: false }, request(10_000));
52
+ await bounded(cancelled, 'Interrupt did not cancel the underlying provider HTTP request');
53
+ assert.deepEqual(await Promise.all([host.outcome(interrupted.id), host.outcome(healthy.id)]), ['interrupted', 'succeeded']);
54
+ assert.ok((await client.session.inbox.list({ sessionID: interrupted.id }, request())).some(item => item.id === queued.id), 'Queued prompt remains pending after interruption');
55
+ assert.ok(!host.requests.some(body => JSON.stringify(body.messages).includes('CTG_FIXTURE_QUEUED')), 'Queued prompt did not resume execution');
56
+ await client.session.inbox.cancel({ sessionID: interrupted.id, inboxID: queued.id }, request());
57
+ const cancellationMs = Math.round(performance.now() - started);
58
+
59
+ // An interrupted session remains usable without restarting its neighbor.
60
+ await client.session.prompt({ sessionID: interrupted.id, text: 'Recover after interruption' }, request());
61
+ assert.equal(await host.outcome(interrupted.id), 'succeeded');
62
+ const history = await client.session.context({ sessionID: interrupted.id }, request());
63
+ assert.ok(history.some(message => message.type === 'idle' && message.outcome === 'interrupted'));
64
+ assert.equal(history.at(-1).outcome, 'succeeded');
65
+ assert.deepEqual(host.providerErrors, []);
66
+ console.log(JSON.stringify({ host: '2.0.32', failure: 'passed', cancellation: 'passed',
67
+ concurrentIsolation: 'passed', recovery: 'passed', queuedInterruption: 'passed', cancellationMs }));
68
+ } catch (error) {
69
+ console.error(host.output);
70
+ throw error;
71
+ } finally {
72
+ await host.close();
73
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /** Build and install the published artifact in isolation, then exercise its owned host. */
3
+ import assert from 'node:assert/strict';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { join, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
11
+ const temp = await mkdtemp(join(tmpdir(), 'ctg-native-package-'));
12
+ const run = (command, args, options = {}) => {
13
+ const result = spawnSync(command, args, { cwd: root, encoding: 'utf8', ...options });
14
+ if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} failed:\n${result.stdout}\n${result.stderr}`);
15
+ return result.stdout;
16
+ };
17
+
18
+ try {
19
+ const packed = JSON.parse(run('npm', ['pack', '--json', '--pack-destination', temp]));
20
+ const tarball = join(temp, packed[0].filename);
21
+ const paths = packed[0].files.map((file) => file.path);
22
+ assert.ok(paths.includes('plugins/carthagent/index.ts'), 'pack must include plugin server entrypoint');
23
+ assert.ok(paths.includes('plugins/carthagent/tui.ts'), 'pack must include plugin TUI entrypoint');
24
+ assert.ok(!paths.some((path) => /(^|\/)node_modules(\/|$)/.test(path)), 'pack must not include node_modules');
25
+ assert.ok(packed[0].size < 30_000_000, `unexpectedly large package tarball: ${packed[0].size} bytes`);
26
+
27
+ const install = join(temp, 'install');
28
+ await import('node:fs/promises').then(({ mkdir }) => mkdir(install));
29
+ run('npm', ['install', '--prefix', install, '--omit=dev', '--no-audit', '--no-fund', tarball]);
30
+ const manifest = JSON.parse(await readFile(join(install, 'node_modules/carthagent/package.json'), 'utf8'));
31
+ assert.equal(manifest.dependencies['@opencode/cli'], '2.0.32');
32
+ const binary = join(install, 'node_modules/@opencode/cli/bin/opencode.exe');
33
+ assert.ok((await stat(binary)).size > 100_000_000, 'native host binary must be materialized by postinstall');
34
+ const version = run(binary, ['--version'], { cwd: install }).trim();
35
+ assert.equal(version, 'opencode v2.0.32');
36
+
37
+ const env = {
38
+ ...process.env,
39
+ CTG_OPENCODE_BIN: binary,
40
+ CTG_EVALUATION_MAX_TURNS: '8',
41
+ CTG_EVALUATION_MAX_COST: '1',
42
+ HOME: join(temp, 'home'),
43
+ XDG_CONFIG_HOME: join(temp, 'config'),
44
+ XDG_DATA_HOME: join(temp, 'data'),
45
+ XDG_STATE_HOME: join(temp, 'state'),
46
+ XDG_CACHE_HOME: join(temp, 'cache'),
47
+ };
48
+ const installed = join(install, 'node_modules/carthagent');
49
+ // The product entrypoint is intentionally parent-composed. Build a disposable
50
+ // host entrypoint that composes the package's exported native integration.
51
+ const installedPlugin = join(installed, '.ctg-production-plugin');
52
+ await mkdir(installedPlugin);
53
+ await writeFile(join(installedPlugin, 'index.ts'), `import { Plugin } from '@opencode/plugin';\nimport { setupCarthagent } from '../plugins/carthagent/src/index';\nexport default Plugin.define({ id: 'carthagent', setup: context => setupCarthagent(context, { delivery: true }) });\n`);
54
+ // Run the acceptance harness from the installed artifact itself so its
55
+ // imports (including @opencode/client) resolve only through that install.
56
+ const composedPlugin = installedPlugin;
57
+ for (const test of ['test-native-host.mjs', 'test-native-controls.mjs', 'test-native-outcomes.mjs', 'test-native-evaluation.mjs', 'test-native-swe.mjs']) {
58
+ run(process.execPath, [join(installed, 'tools', test), composedPlugin], { cwd: install, env });
59
+ }
60
+ run(process.execPath, [join(installed, 'tools', 'test-native-swe-host.mjs'), join(installed, 'plugins/carthagent')], { cwd: install, env });
61
+ for (const scenario of ['standard', 'strict', 'raw', 'budget', 'mixed', 'concurrent']) {
62
+ run(process.execPath, [join(installed, 'tools', 'test-native-delivery-host.mjs'), composedPlugin], {
63
+ cwd: install, env: { ...env, CTG_NATIVE_SCENARIO: scenario },
64
+ });
65
+ }
66
+ run(process.execPath, [join(installed, 'tools', 'test-native-report.mjs'), join(installed, 'tools', 'native-report-plugin')], { cwd: install, env });
67
+ console.log(JSON.stringify({ tarball: packed[0].filename, unpackedSize: packed[0].unpackedSize, binary, version }, null, 2));
68
+ } finally {
69
+ await rm(temp, { recursive: true, force: true });
70
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /** Unix-only PTY helper; CI skips this test explicitly on Windows. */
3
+ import { spawnSync } from 'node:child_process';
4
+ import { resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ if (process.platform === 'win32') {
8
+ console.log('Native PTY smoke skipped: the Python PTY implementation is Unix-only.');
9
+ process.exit(0);
10
+ }
11
+ const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
12
+ const binary = process.env.CTG_OPENCODE_BIN ?? resolve(root, 'node_modules/@opencode/cli/bin/opencode.exe');
13
+ const plugin = resolve(root, 'plugins/carthagent');
14
+ const testPlugin = resolve(plugin, 'test-plugin');
15
+ const result = spawnSync('python3', [resolve(root, 'tools/native-pty-smoke.py'), '--binary', binary, '--plugin', plugin, '--delivery-plugin', testPlugin], {
16
+ cwd: root,
17
+ stdio: 'inherit',
18
+ });
19
+ if (result.error) throw result.error;
20
+ if (result.status !== 0) process.exit(result.status ?? 1);
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /** Exercise the delivery report output through OpenCode's real RPC validator. */
3
+ import assert from 'node:assert/strict';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { writeFile } from 'node:fs/promises';
6
+ import { join } from 'node:path';
7
+ import { checkDigest, fingerprint } from '../lib/delivery.mjs';
8
+ import { createReport } from '../lib/reports.mjs';
9
+ import { selectReport } from '../lib/workspace.mjs';
10
+ import { nativeFixture } from './native-fixture.mjs';
11
+
12
+ const plugin = process.argv[2] ?? join(import.meta.dirname, 'native-report-plugin');
13
+ const host = await nativeFixture({ plugin });
14
+ try {
15
+ execFileSync('git', ['init', '-q'], { cwd: host.cwd });
16
+ await writeFile(join(host.cwd, 'source.txt'), 'original\n');
17
+ const activationDeadline = Date.now() + 15_000;
18
+ while (true) {
19
+ const inventory = await host.client.plugin.list({ location: host.location }, host.request());
20
+ const pluginState = inventory.data.find(item => item.id === 'carthagent-report-fixture')?.state.status;
21
+ if (pluginState === 'active') break;
22
+ if (Date.now() >= activationDeadline) throw Error(`Report fixture plugin did not activate: ${JSON.stringify(inventory.data)}\n${host.output}`);
23
+ await new Promise(done => setTimeout(done, 50));
24
+ }
25
+ const session = await host.client.session.create({ location: host.location, model: host.model, title: 'Report projection' }, host.request());
26
+ const read = async () => (await host.client.rpc.call({
27
+ rpcID: 'carthagent.workspace', method: 'delivery', input: { sessionID: session.id }, location: host.location,
28
+ }, host.request())).output;
29
+ const probe = method => host.client.rpc.call({
30
+ rpcID: 'carthagent.report-schema-probe', method, input: {}, location: host.location,
31
+ }, host.request());
32
+ const check = { id: 'unit', kind: 'test', argv: ['node', '--test'], timeoutSeconds: 20 };
33
+ const plan = {
34
+ goal: 'Show authoritative report data', verification: 'required', artifacts: ['.'], outputs: [],
35
+ steps: [{ id: 'implement', title: 'Implement the report view', dependsOn: [], checks: ['unit'] }],
36
+ acceptance: [{ requirement: 'Report is visible', checks: ['unit'] }], checks: [check],
37
+ design: { components: [{ id: 'core', responsibility: 'Project report state', layer: 'core', excludes: ['UI'], dependencies: [], requirements: ['Report is visible'] }], ports: [] },
38
+ };
39
+ // Fixture report receipts exercise projection/schema handling; they are not
40
+ // evidence of real check execution or a substitute for delivery enforcement.
41
+ const runId = 'projection-fixture';
42
+ const state = {
43
+ runId, revision: 1, status: 'implementing', workflowMode: 'standard', plan,
44
+ stepStatus: { step0: 'done' },
45
+ reviewEvidence: { runId, revision: 1, fingerprint: fingerprint(host.cwd, ['.']) },
46
+ evidence: { unit: {
47
+ runId, revision: 1, executedRevision: 1, checkDigest: checkDigest(check),
48
+ fingerprint: fingerprint(host.cwd, ['.']), passed: true, durationMs: 2, code: 0,
49
+ outputTail: 'PRIVATE_REPORT_LOG_SENTINEL',
50
+ logPath: join(host.cwd, '.harness', session.id, runId, 'logs', 'unit.log'),
51
+ } },
52
+ };
53
+ const report = createReport(host.cwd, state, session.id);
54
+ selectReport(host.cwd, report.path);
55
+ const current = await read();
56
+ assert.equal(current.plan.goal, plan.goal);
57
+ assert.equal(current.freshness, 'current', JSON.stringify(current));
58
+ assert.equal(current.evidence[0].status, 'fresh');
59
+ assert.equal(current.fingerprint.length, 64);
60
+ assert.ok(current.workflow.nodes.length > 0);
61
+ assert.equal(current.architecture.nodes[0].title, 'Project report state');
62
+ assert.ok(!JSON.stringify(current).includes('PRIVATE_REPORT_LOG_SENTINEL'));
63
+ assert.ok(!JSON.stringify(current).includes(host.cwd));
64
+ await assert.rejects(probe('incompleteVerified'), error => error.type === 'rpc.invalid_output');
65
+ const invalidWithFields = await probe('invalidWithFullFields');
66
+ assert.deepEqual(invalidWithFields.output, { status: 'invalid' });
67
+ assert.deepEqual((await probe('validUnavailable')).output, { status: 'unavailable' });
68
+ assert.deepEqual((await probe('validInvalid')).output, { status: 'invalid' });
69
+ assert.equal((await probe('validActive')).output.status, 'active');
70
+
71
+ await writeFile(join(host.cwd, 'source.txt'), 'modified\n');
72
+ const stale = await read();
73
+ assert.equal(stale.freshness, 'stale');
74
+ assert.equal(stale.evidence[0].status, 'stale');
75
+ for (const evidence of [{}, { unit: { ...state.evidence.unit, passed: false } }]) {
76
+ await writeFile(report.path, JSON.stringify({ ...state, status: 'verified', evidence }));
77
+ assert.notEqual((await read()).status, 'verified', 'missing or failed evidence cannot inherit a persisted verified label');
78
+ }
79
+ const neighbor = await host.client.session.create({ location: host.location, model: host.model, title: 'Other report owner' }, host.request());
80
+ selectReport(host.cwd, createReport(host.cwd, state, neighbor.id).path);
81
+ assert.deepEqual(await read(), { status: 'unavailable' });
82
+ selectReport(host.cwd, report.path);
83
+ await writeFile(report.path, '{corrupt');
84
+ assert.deepEqual(await read(), { status: 'invalid' });
85
+ console.log(JSON.stringify({ host: '2.0.32', deliveryOutputSchema: 'passed', freshness: 'passed', verifiedFailClosed: 'passed', ownership: 'passed' }));
86
+ } finally {
87
+ await host.close();
88
+ }