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
package/lib/review.mjs ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Self-review loop: after a substantial change, the user can opt in to a
3
+ * PR ↔ fresh-context review ↔ fixes loop. The current agent pushes a branch
4
+ * and opens a PR; `ctg review <pr>` then runs a brand-new agent process with
5
+ * no shared context to review it, and the working agent applies the findings.
6
+ *
7
+ * Default behavior is a single tri-state: ask (offer at the end of a major
8
+ * change), yes (always start the loop), no (never). Resolution order:
9
+ * explicit flag (--review / --delivery-review) > ~/.carthagent/config.json > 'ask'.
10
+ */
11
+ import { readFileSync } from 'node:fs';
12
+ import { join, dirname, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import os from 'node:os';
15
+ import { atomicJson } from './delivery.mjs';
16
+
17
+ export const REVIEW_MODES = ['ask', 'yes', 'no'];
18
+ /** Persistent local PR review admission limit; not a security boundary. */
19
+ export const MAX_REVIEW_ROUNDS = 3;
20
+
21
+ export function normalizeReviewMode(value) {
22
+ const s = String(value ?? '').trim().toLowerCase();
23
+ return REVIEW_MODES.includes(s) ? s : null;
24
+ }
25
+
26
+ /** carthagent's user-level config file (CARTHAGENT_CONFIG overrides for tests). */
27
+ export function carthagentConfigPath() {
28
+ return process.env.CARTHAGENT_CONFIG || join(os.homedir(), '.carthagent', 'config.json');
29
+ }
30
+
31
+ export function loadCarthagentConfig(path = carthagentConfigPath()) {
32
+ try {
33
+ const data = JSON.parse(readFileSync(path, 'utf8'));
34
+ return data && typeof data === 'object' ? data : {};
35
+ } catch {
36
+ return {};
37
+ }
38
+ }
39
+
40
+ /** Merge a patch into ~/.carthagent/config.json atomically. Returns the new config. */
41
+ export function saveCarthagentConfig(patch, path = carthagentConfigPath()) {
42
+ const config = { ...loadCarthagentConfig(path), ...patch };
43
+ atomicJson(path, config);
44
+ return config;
45
+ }
46
+
47
+ /** flag (explicit CLI/extension flag) > config file > 'ask'. */
48
+ export function resolveReviewMode(flagValue, config = loadCarthagentConfig()) {
49
+ return normalizeReviewMode(flagValue) ?? normalizeReviewMode(config?.review) ?? 'ask';
50
+ }
51
+
52
+ /** Resolve sticky theme/model defaults: explicit flag > config > undefined.
53
+ * Undefined falls through to the built-in default (carthage theme, agent's
54
+ * default model) so we don't pass a stale `--model`/`--theme` to the engine. */
55
+ export function resolveStickyDefaults(opts, config = loadCarthagentConfig()) {
56
+ return {
57
+ theme: opts?.theme || config?.theme || undefined,
58
+ model: opts?.model || config?.model || undefined,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Pure offer predicate shared by the extension and the console: a self-review
64
+ * offer is only made in ask mode, once per run, when the run produced a
65
+ * verified delivery or real file edits, and no loop is already active.
66
+ */
67
+ export function shouldOfferReview({ mode, verified, writes, loopActive, offeredRuns, runKey }) {
68
+ if (mode !== 'ask' || loopActive) return false;
69
+ if (!verified && !(writes > 0)) return false;
70
+ return !(offeredRuns?.has?.(runKey));
71
+ }
72
+
73
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
74
+ const CTG_BIN = join(ROOT, 'bin', 'ctg.mjs');
75
+
76
+ /**
77
+ * Instructions handed to the working agent when a review loop starts (via the
78
+ * extension's kickoff message or the console's /review + ^y paths).
79
+ */
80
+ export function reviewKickoff(ctgBin = CTG_BIN) {
81
+ return `The user opted in to a self-review loop — run it now.
82
+
83
+ 1. Commit the finished work on a review branch (never the default branch), push it, and open a pull request (\`gh pr create --fill\` or equivalent). If the workspace is not a git repo, has no remote, or \`gh\` is unavailable/unauthenticated, tell the user and stop — do not simulate a review.
84
+ 2. Run: node "${ctgBin}" review <pr-number-or-url>
85
+ This spawns a fresh-context reviewer that inspects the PR and prints prioritized findings, ending with VERDICT: APPROVE (exit 0) or VERDICT: CHANGES-REQUESTED (exit 1).
86
+ 3. On CHANGES-REQUESTED: address each finding, commit, push, and run the same review command again on the same PR. On APPROVE: summarize the outcome and stop.
87
+ 4. Stop after at most ${MAX_REVIEW_ROUNDS} review rounds — the CLI persists admissions per PR in this workspace, including failed attempts. At the limit, summarize remaining findings and escalate to the user instead of looping forever.`;
88
+ }
89
+
90
+ /** Prompt for the detached fresh-context reviewer subprocess (`ctg review <pr>`). */
91
+ export function reviewerPrompt({ prRef, cwd, meta = {} }) {
92
+ const context = [
93
+ meta.title ? `PR title: ${meta.title}` : '',
94
+ meta.url ? `PR url: ${meta.url}` : '',
95
+ meta.headRefName ? `branches: ${meta.headRefName} → ${meta.baseRefName}` : '',
96
+ ].filter(Boolean).join('\n');
97
+ return `You are a fresh-context code reviewer. Review pull request ${JSON.stringify(String(prRef))} in the repository at ${JSON.stringify(cwd)}.
98
+ ${context ? context + '\n' : ''}
99
+ Inspect the change with \`gh pr view\`, \`gh pr diff\` and \`gh pr checks\` for this PR, and read the touched files in the working tree for surrounding context. Do not modify files, commit, push, or check out branches — review only.
100
+
101
+ Hunt for: correctness bugs and broken edge cases, security issues, missing test coverage for changed behavior, and violations of this repository's existing conventions. Skip style nitpicks that tooling already enforces. Verify every claim against the actual code before reporting it — no speculative findings.
102
+
103
+ Report format: a prioritized findings list, each entry \`file:line — severity — issue — suggested fix\` (mark uncertain findings as uncertain). Put the complete findings list at the END of your reply so it survives output truncation, then finish with exactly one line:
104
+ VERDICT: APPROVE — no blocking findings
105
+ VERDICT: CHANGES-REQUESTED — at least one blocking finding`;
106
+ }
107
+
108
+ /** Extract the reviewer's verdict from its output tail — the LAST VERDICT line
109
+ * wins, so format examples quoted earlier can't shadow the real verdict. */
110
+ export function parseVerdict(output) {
111
+ const last = String(output ?? '').trim().split(/\r?\n/).at(-1);
112
+ const match = /^VERDICT:\s*(APPROVE|CHANGES-REQUESTED)(?:\s+—\s+[^\r\n]+)?$/i.exec(last);
113
+ return match ? match[1].toUpperCase() : null;
114
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Scenario manifests: pluggable task profiles for the evaluation runner.
3
+ *
4
+ * Each scenarios/<name>/ directory holds a scenario.json plus its graders,
5
+ * seed files, and context. The harness (evaluate.mjs) stays domain-agnostic —
6
+ * a game, a CLI task, or any future domain is just another manifest, i.e. a
7
+ * selectable path through the same delivery plan → check → finish pipeline.
8
+ *
9
+ * scenario.json shape:
10
+ * {
11
+ * "name": "game",
12
+ * "description": "…",
13
+ * "prompt": "the single user prompt", // or { "file": "prompt.md" }
14
+ * "setup": "text appended to the shared execution context (both modes)",
15
+ * "context": ["context.md"], // files joined into --delivery-context (custom mode)
16
+ * "skills": ["game-development"], // skill dirs whose SKILL.md is inlined into context
17
+ * "seeds": [{ "from": "seed/file.ts", "to": "src/index.ts" }],
18
+ * "runtimes": { "python": "/path/to/python" }, // extra runtime placeholders (optional)
19
+ * "developmentChecks": [check], // required validators during the run
20
+ * "holdoutChecks": [check], // scored after the run, unseen by the model
21
+ * }
22
+ *
23
+ * check = { "id", "kind", "argv": [...], "timeoutSeconds" } — argv supports
24
+ * placeholders: {python} {node} {root} {home} {cwd} {scenario}, plus any key
25
+ * declared in the scenario's "runtimes" map (e.g. {rust}, {go}).
26
+ */
27
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
28
+ import { isAbsolute, join, relative, resolve } from 'node:path';
29
+
30
+ export function listScenarios(root) {
31
+ const dir = join(root, 'scenarios');
32
+ if (!existsSync(dir)) return [];
33
+ return readdirSync(dir, { withFileTypes: true })
34
+ .filter(d => d.isDirectory() && existsSync(join(dir, d.name, 'scenario.json')))
35
+ .map(d => d.name);
36
+ }
37
+
38
+ export function loadScenario(root, name) {
39
+ const scenariosRoot = resolve(root, 'scenarios');
40
+ const dir = resolve(scenariosRoot, name);
41
+ const local = relative(scenariosRoot, dir);
42
+ if (!name || local === '..' || local.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(local)) throw Error(`Invalid scenario name: ${JSON.stringify(name)}`);
43
+ const manifestPath = join(dir, 'scenario.json');
44
+ if (!existsSync(manifestPath)) {
45
+ const known = listScenarios(root);
46
+ throw Error(`Unknown scenario ${JSON.stringify(name)}. Available: ${known.join(', ') || '(none — expected scenarios/<name>/scenario.json beside evaluate.mjs)'}`);
47
+ }
48
+ const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
49
+ if (typeof m.name === 'string' && m.name !== name) throw Error(`Scenario ${name} manifest name mismatch: ${m.name}`);
50
+ const prompt = typeof m.prompt === 'string'
51
+ ? m.prompt
52
+ : m.prompt?.file ? readFileSync(join(dir, m.prompt.file), 'utf8').trim()
53
+ : null;
54
+ if (!prompt) throw Error(`Scenario ${name} needs a prompt (string or {file})`);
55
+
56
+ const check = c => {
57
+ if (!c.id || !Array.isArray(c.argv) || !c.argv.every(a => typeof a === 'string' && a.length)) throw Error(`Scenario ${name}: invalid check ${JSON.stringify(c?.id)}`);
58
+ return { kind: 'runtime', timeoutSeconds: 90, ...c };
59
+ };
60
+ const developmentChecks = (m.developmentChecks || []).map(check);
61
+ const holdoutChecks = (m.holdoutChecks || []).map(check);
62
+ if (!developmentChecks.length && !holdoutChecks.length) throw Error(`Scenario ${name} declares no checks`);
63
+
64
+ // Resolve everything path-shaped relative to the scenario dir now; {cwd}
65
+ // and {python} stay as placeholders — they differ per trial run.
66
+ const contextParts = [];
67
+ for (const skill of m.skills || []) {
68
+ const skillDir = join(root, 'skills', skill);
69
+ const skillMd = join(skillDir, 'SKILL.md');
70
+ if (!existsSync(skillMd)) throw Error(`Scenario ${name}: missing skill ${skillDir}`);
71
+ contextParts.push(readFileSync(skillMd, 'utf8'), `Skill-relative assets/scripts resolve under: ${skillDir}`);
72
+ }
73
+ for (const file of m.context || []) contextParts.push(readFileSync(join(dir, file), 'utf8'));
74
+
75
+ return {
76
+ name,
77
+ dir,
78
+ description: m.description || '',
79
+ prompt,
80
+ setup: m.setup || '',
81
+ context: contextParts.join('\n\n'),
82
+ seeds: (m.seeds || []).map(s => ({
83
+ from: expandPlaceholders(s.from, { root, home: root, scenario: dir }),
84
+ to: s.to,
85
+ })),
86
+ developmentChecks,
87
+ holdoutChecks,
88
+ needsPython: JSON.stringify(m).includes('{python}'),
89
+ runtimes: m.runtimes || {},
90
+ };
91
+ }
92
+
93
+ /** Substitute {python} {node} {root} {home} {cwd} {scenario} and any scenario-declared runtime keys in a string/argv. */
94
+ export function expandPlaceholders(value, vars) {
95
+ const sub = s => s.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`);
96
+ if (typeof value === 'string') return sub(value);
97
+ if (Array.isArray(value)) return value.map(v => expandPlaceholders(v, vars));
98
+ return value;
99
+ }
@@ -0,0 +1,44 @@
1
+ import { readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import os from 'node:os';
4
+ import { CloudClient, cloudControlUrl, oauthCredentials } from './cloud/client.mjs';
5
+ import { CARTHAGENT_SHIP_PROVIDER_ID } from './providers/names.mjs';
6
+
7
+ // Refresh a little early so a token minted minutes ago does not expire
8
+ // mid-request; the engine applies its own refresh to managed dispatch anyway.
9
+ const REFRESH_SKEW_MS = 60_000;
10
+
11
+ export function shipAgentDir(env = process.env) {
12
+ return env.CARTHAGENT_CODING_AGENT_DIR || env.PI_CODING_AGENT_DIR || join(os.homedir(), '.carthagent', 'agent');
13
+ }
14
+
15
+ export function readShipAuth({ agentDir = shipAgentDir(), providerId = CARTHAGENT_SHIP_PROVIDER_ID } = {}) {
16
+ try {
17
+ const auth = JSON.parse(readFileSync(join(agentDir, 'auth.json'), 'utf8'));
18
+ return auth?.[providerId] ?? null;
19
+ } catch { return null; }
20
+ }
21
+
22
+ /**
23
+ * Resolve a usable Carthagent Ship access token from the engine credential
24
+ * store (auth.json), refreshing through the control plane when the stored
25
+ * access token is near expiry. Returns null when no Ship OAuth credential
26
+ * exists. The refreshed credential is written back so the engine's own
27
+ * dispatch sees it too.
28
+ */
29
+ export async function resolveShipAccessToken({ agentDir = shipAgentDir(), providerId = CARTHAGENT_SHIP_PROVIDER_ID, env = process.env, fetchImpl = globalThis.fetch, now = Date.now, signal } = {}) {
30
+ const authPath = join(agentDir, 'auth.json');
31
+ let auth;
32
+ try { auth = JSON.parse(readFileSync(authPath, 'utf8')); } catch { return null; }
33
+ const credential = auth?.[providerId];
34
+ if (credential?.type !== 'oauth' || typeof credential.access !== 'string' || typeof credential.refresh !== 'string') return null;
35
+ if (Number(credential.expires) > now() + REFRESH_SKEW_MS) return credential.access;
36
+ const client = new CloudClient({ baseUrl: cloudControlUrl(env), fetchImpl });
37
+ const tokens = await client.refresh({ refreshToken: credential.refresh, signal });
38
+ const next = oauthCredentials(tokens, now());
39
+ auth[providerId] = { type: 'oauth', ...next };
40
+ const temp = `${authPath}.${process.pid}.tmp`;
41
+ writeFileSync(temp, JSON.stringify(auth, null, 2));
42
+ renameSync(temp, authPath);
43
+ return next.access;
44
+ }
@@ -0,0 +1,59 @@
1
+ import { ADVISOR_QUESTIONS } from './plan-advisor.mjs';
2
+ import { cloudGatewayUrl } from './providers/experiential.mjs';
3
+ import { resolveShipAccessToken } from './ship-auth.mjs';
4
+
5
+ const MAX_BODY_BYTES = 64000;
6
+ const MAX_RESPONSE_BYTES = 65536;
7
+
8
+ /**
9
+ * Jev plan advice through Carthagent Ship's managed /v1/jev endpoint.
10
+ * The backend forwards to Experiential's free System One endpoint
11
+ * (jev-latest) outside the billing gateway, so advice never draws on
12
+ * the caller's managed usage. Same Noul wire shape as the other adapters.
13
+ */
14
+ export function createShipJev({ accessToken, baseUrl, fetchImpl = globalThis.fetch, timeoutMs = 15000, signal } = {}) {
15
+ if (typeof accessToken !== 'string' || !accessToken.trim()) throw Error('Ship credential required');
16
+ if (typeof baseUrl !== 'string' || !baseUrl.trim()) throw Error('Ship endpoint URL required');
17
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw Error('Invalid Jev timeout');
18
+ const url = `${baseUrl.replace(/\/+$/, '')}/jev`;
19
+ return { async classify(plan) {
20
+ const body = JSON.stringify({ state: { goal: plan.goal, acceptance: plan.acceptance, checks: plan.checks, outputs: plan.outputs, assumptions: plan.assumptions, design: plan.design, workflow: plan.workflow }, questions: ADVISOR_QUESTIONS });
21
+ if (Buffer.byteLength(body) > MAX_BODY_BYTES) throw Error('Jev input exceeds 64KB');
22
+ const controller = new AbortController();
23
+ let timer;
24
+ const timeout = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(Error('Jev timeout')); }, timeoutMs); });
25
+ try { return await Promise.race([timeout, (async () => {
26
+ const response = await fetchImpl(url, { method: 'POST', redirect: 'error', headers: { 'content-type': 'application/json', authorization: `Bearer ${accessToken}` }, body, signal: AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]) });
27
+ if (!response.ok) { await response.body?.cancel(); throw Error('Jev HTTP failure'); }
28
+ const reader = response.body?.getReader(); if (!reader) throw Error('Missing Jev body');
29
+ let size = 0; const chunks = [];
30
+ try { for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.byteLength; if (size > MAX_RESPONSE_BYTES) { await reader.cancel(); throw Error('Jev response too large'); } chunks.push(Buffer.from(value)); } }
31
+ finally { reader.releaseLock(); }
32
+ const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
33
+ if (typeof payload.model !== 'string' || !/jev/i.test(payload.model)) throw Error('Invalid Jev model');
34
+ const probabilities = {};
35
+ for (const key of Object.keys(ADVISOR_QUESTIONS)) {
36
+ const answer = payload.answers?.[key];
37
+ if (answer?.type !== 'noul' || !Number.isFinite(answer.noul) || answer.noul < 0 || answer.noul > 1) throw Error('Invalid Jev answer');
38
+ probabilities[key] = answer.noul;
39
+ }
40
+ const usage = payload.usage;
41
+ return { model: payload.model, probabilities, usage: usage ? { inputTokens: usage.input_tokens ?? usage.prompt_tokens ?? null, outputTokens: usage.output_tokens ?? usage.completion_tokens ?? null } : null, costUsd: Number.isFinite(usage?.cost) && usage.cost >= 0 ? usage.cost : 0 };
42
+ })()]); } finally { clearTimeout(timer); controller.abort(); }
43
+ } };
44
+ }
45
+
46
+ /**
47
+ * Ship advisor with lazy credential resolution: the stored OAuth access token
48
+ * (refreshed when needed) is fetched once per advisor, so classify calls see
49
+ * the current credential without holding provider keys locally.
50
+ */
51
+ export function createShipAdvisor({ env = process.env, fetchImpl = globalThis.fetch, signal, agentDir, now } = {}) {
52
+ let tokenPromise = null;
53
+ return { async classify(plan) {
54
+ tokenPromise ??= resolveShipAccessToken({ env, fetchImpl, agentDir, now, signal });
55
+ const accessToken = await tokenPromise;
56
+ if (!accessToken) throw Error('Ship credential unavailable');
57
+ return createShipJev({ accessToken, baseUrl: cloudGatewayUrl(env), fetchImpl, signal }).classify(plan);
58
+ } };
59
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Carthagent Ship telemetry context — the metadata-only headers the control
3
+ * plane uses for product analytics (threads, installs, run modes). No prompt
4
+ * or response content is ever collected, and everything here is best-effort:
5
+ * a missing or unreadable install id never blocks a request.
6
+ */
7
+ import { linkSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
+ import { createHash, randomUUID } from 'node:crypto';
10
+ import { shipAgentDir } from './ship-auth.mjs';
11
+ import { CARTHAGENT_SHIP_PROVIDER_ID } from './providers/names.mjs';
12
+
13
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
+ const RUN_MODES = new Set(['interactive', 'print', 'rpc']);
15
+ // Same env keys the console treats as a usable BYOK credential.
16
+ const BYOK_ENV_KEYS = [
17
+ 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY',
18
+ 'OPENROUTER_API_KEY', 'XAI_API_KEY', 'EXPLABS_API_KEY',
19
+ 'GROQ_API_KEY', 'MISTRAL_API_KEY', 'DEEPSEEK_API_KEY',
20
+ ];
21
+
22
+ export function installIdPath(agentDir = shipAgentDir()) {
23
+ return join(agentDir, 'install-id');
24
+ }
25
+
26
+ function publishInstallId(path, value) {
27
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
28
+ try {
29
+ writeFileSync(temp, `${value}\n`, { mode: 0o600, flag: 'wx' });
30
+ renameSync(temp, path);
31
+ } finally {
32
+ try { unlinkSync(temp); } catch { /* rename already consumed it */ }
33
+ }
34
+ }
35
+
36
+ function deterministicRepairId(agentDir) {
37
+ const hex = createHash('sha256').update(`${resolve(agentDir)}\0ship-install-id-repair`).digest('hex').slice(0, 32).split('');
38
+ hex[12] = '4';
39
+ hex[16] = 'a';
40
+ const value = hex.join('');
41
+ return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
42
+ }
43
+
44
+ function readUuid(path) {
45
+ try {
46
+ const value = String(readFileSync(path, 'utf8')).trim().toLowerCase();
47
+ return UUID.test(value) ? value : '';
48
+ } catch { return ''; }
49
+ }
50
+
51
+ function publishImmutable(path, value) {
52
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
53
+ try {
54
+ writeFileSync(temp, `${value}\n`, { mode: 0o600, flag: 'wx' });
55
+ try { linkSync(temp, path); } catch (error) {
56
+ if (error?.code !== 'EEXIST') throw error;
57
+ }
58
+ } finally {
59
+ try { unlinkSync(temp); } catch { /* link already removed or temp absent */ }
60
+ }
61
+ }
62
+
63
+ /** Persistent anonymous install id: minted on first run, reused after. */
64
+ export function shipInstallId({ agentDir = shipAgentDir(), id = randomUUID } = {}) {
65
+ const path = installIdPath(agentDir);
66
+ const seed = `${path}.seed`;
67
+ const recovery = `${seed}.recovery`;
68
+ const existing = readUuid(path);
69
+ try {
70
+ mkdirSync(agentDir, { recursive: true });
71
+ // Recovery is an immutable create-only hard link. Priority preserves an
72
+ // established recovery/seed identity, then a valid legacy primary, and
73
+ // only then mints. Corrupt copies are never rewritten or lock-stolen.
74
+ const recoveryId = readUuid(recovery);
75
+ const seedId = readUuid(seed);
76
+ const candidate = recoveryId || seedId || existing || String(id()).toLowerCase();
77
+ const minted = UUID.test(candidate) ? candidate : deterministicRepairId(agentDir);
78
+ try { publishImmutable(recovery, minted); } catch { /* fall back to shared legacy identity or deterministic ID */ }
79
+ const publishedRecovery = readUuid(recovery);
80
+ // A newly minted value is usable only if an immutable shared copy was
81
+ // published and read back. Unsupported hard links fall back to an already
82
+ // persisted identity, or a deterministic per-directory UUID, never this
83
+ // process's random candidate.
84
+ const identity = publishedRecovery || recoveryId || seedId || existing || deterministicRepairId(agentDir);
85
+ // Keep the original seed useful when it is absent; never replace a
86
+ // malformed seed. The recovery seed above remains the canonical fallback.
87
+ try { publishImmutable(seed, identity); } catch { /* existing seed or storage error */ }
88
+ const canonicalIdentity = readUuid(recovery) || readUuid(seed) || existing || identity;
89
+ publishInstallId(path, canonicalIdentity);
90
+ return canonicalIdentity;
91
+ } catch {
92
+ return existing || readUuid(recovery) || readUuid(seed) || deterministicRepairId(agentDir); // storage remains best-effort
93
+ }
94
+ }
95
+
96
+ const hasCredential = credential => {
97
+ if (typeof credential === 'string') return Boolean(credential.trim());
98
+ if (!credential || typeof credential !== 'object') return false;
99
+ return ['key', 'apiKey', 'api_key', 'access', 'refresh'].some(field => typeof credential[field] === 'string' && credential[field].trim());
100
+ };
101
+
102
+ /** 'ship' | 'byok' | 'ship+byok' | 'none' from the local credential store. */
103
+ export function providerMode({ agentDir = shipAgentDir(), env = process.env } = {}) {
104
+ let ship = false;
105
+ let byok = BYOK_ENV_KEYS.some(key => env?.[key]?.trim());
106
+ try {
107
+ const stored = JSON.parse(readFileSync(join(agentDir, 'auth.json'), 'utf8'));
108
+ if (stored && typeof stored === 'object' && !Array.isArray(stored)) {
109
+ for (const [providerId, credential] of Object.entries(stored)) {
110
+ if (!hasCredential(credential)) continue;
111
+ // Only the OAuth credential is a Ship account; a direct api_key under
112
+ // this provider id goes through the BYOK gateway.
113
+ if (providerId === CARTHAGENT_SHIP_PROVIDER_ID && credential?.type === 'oauth') ship = true;
114
+ else byok = true;
115
+ }
116
+ }
117
+ } catch { /* no stored oauth/api-key credentials */ }
118
+ try {
119
+ const providers = JSON.parse(readFileSync(join(agentDir, 'models.json'), 'utf8'))?.providers;
120
+ if (providers && typeof providers === 'object' && !Array.isArray(providers)) {
121
+ // Any credentialed models.json provider is a direct (BYOK) credential —
122
+ // Ship OAuth lives in auth.json only.
123
+ for (const providerId of Object.keys(providers)) {
124
+ if (hasCredential(providers[providerId])) byok = true;
125
+ }
126
+ }
127
+ } catch { /* no persisted model-store credentials */ }
128
+ return ship && byok ? 'ship+byok' : ship ? 'ship' : byok ? 'byok' : 'none';
129
+ }
130
+
131
+ /** Headers for the daily release check — doubles as the install ping. */
132
+ export function installPingHeaders({ env = process.env, agentDir = shipAgentDir(env), version = null } = {}) {
133
+ return {
134
+ 'x-carthagent-install-id': shipInstallId({ agentDir }),
135
+ 'x-carthagent-version': version || '',
136
+ 'x-carthagent-os': process.platform,
137
+ 'x-carthagent-arch': process.arch,
138
+ 'x-carthagent-node': process.versions.node,
139
+ 'x-carthagent-provider-mode': providerMode({ agentDir, env }),
140
+ };
141
+ }
142
+
143
+ // Per-process Ship request context. The console sets CARTHAGENT_RUN_MODE when
144
+ // it spawns the engine; the extension fills the session fields on lifecycle
145
+ // events, and every managed request is stamped with the current values.
146
+ const sessionContext = { threadId: null, turn: 0, installId: null };
147
+
148
+ /** Adopt the engine's session id as the Ship thread id (minted if absent). */
149
+ export function noteEngineSession(sessionId, { id = randomUUID } = {}) {
150
+ const next = UUID.test(String(sessionId || '')) ? String(sessionId).toLowerCase() : String(id()).toLowerCase();
151
+ // A session_start for the same id (extension reload) keeps the turn count.
152
+ if (sessionContext.threadId === next) return;
153
+ sessionContext.threadId = next;
154
+ sessionContext.turn = 0;
155
+ }
156
+
157
+ /** Each user turn starts a new engine run — advance the turn counter. */
158
+ export function noteAgentTurn() {
159
+ sessionContext.turn += 1;
160
+ }
161
+
162
+ /** Headers stamped on every managed Ship gateway request. */
163
+ export function shipRequestHeaders({ env = process.env, agentDir = shipAgentDir(env), version = null } = {}) {
164
+ if (!sessionContext.threadId) noteEngineSession(null);
165
+ sessionContext.installId ??= shipInstallId({ agentDir });
166
+ const headers = {
167
+ 'x-carthagent-thread-id': sessionContext.threadId,
168
+ 'x-carthagent-turn': String(sessionContext.turn),
169
+ 'x-carthagent-install-id': sessionContext.installId,
170
+ 'x-carthagent-version': version || '',
171
+ 'x-carthagent-os': process.platform,
172
+ };
173
+ const runMode = String(env.CARTHAGENT_RUN_MODE || '').trim();
174
+ if (RUN_MODES.has(runMode)) headers['x-carthagent-run-mode'] = runMode;
175
+ return headers;
176
+ }
177
+
178
+ /** Test hook: clear the per-process context. */
179
+ export function resetShipContext() {
180
+ sessionContext.threadId = null;
181
+ sessionContext.turn = 0;
182
+ sessionContext.installId = null;
183
+ }