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,46 @@
1
+ import { createCodeIndex } from "../../../../lib/codegraph.mjs"
2
+ import { boundedCodeNavResult } from "../../../../lib/codegraph-query.mjs"
3
+ import { resolve } from "node:path"
4
+ import type { Context } from "@opencode/plugin/promise/plugin"
5
+ import type { Tool } from "@opencode/schema/tool"
6
+
7
+ type CodeNavInput = { op: string; name?: string; path?: string; container?: string; limit?: number; offset?: number }
8
+
9
+ const schema = {
10
+ type: "object", additionalProperties: false, required: ["op"],
11
+ properties: {
12
+ op: { type: "string", enum: ["definition", "symbols", "callers", "callees", "imports", "importers", "outline", "status"] },
13
+ name: { type: "string" }, path: { type: "string" }, container: { type: "string" },
14
+ limit: { type: "integer", minimum: 1, maximum: 200, default: 50 }, offset: { type: "integer", minimum: 0, default: 0 },
15
+ },
16
+ }
17
+ const result = (value: string) => ({ content: value })
18
+
19
+ /** Register the structural navigator in the OpenCode server tool registry. */
20
+ export async function registerCodegraph(ctx: Context, options: { maxFiles?: number } = {}) {
21
+ const indexes = new Map<string, Promise<Awaited<ReturnType<typeof createCodeIndex>>>>()
22
+ const registration = await ctx.tool.transform(editor => {
23
+ editor.namespace?.({ name: "carthagent", description: "Carthagent code navigation" })
24
+ editor.add({
25
+ name: "code_nav", description: "Read-only structural graph for JS/TS/Python/Rust. Use definition/symbols/outline to locate code and callers/importers before changing APIs. Calls are name-based candidates, not resolved bindings. Results include coverage, total and nextOffset; page or filter incomplete answers.",
26
+ input: schema as Tool.ValueSchema<CodeNavInput>, options: { namespace: "carthagent", codemode: true, permission: "carthagent_code_nav" },
27
+ execute: async (rawInput, toolContext) => {
28
+ const input = rawInput as CodeNavInput
29
+ const session = await ctx.session.get({ sessionID: toolContext.sessionID })
30
+ const root = resolve(session.location.directory, session.subpath || ".")
31
+ let index = indexes.get(root)
32
+ if (!index) {
33
+ const maxFiles = options.maxFiles ?? 8000
34
+ if (!Number.isSafeInteger(maxFiles) || maxFiles < 1) throw Error("codegraph maxFiles must be a positive safe integer")
35
+ index = createCodeIndex(root, { maxFiles })
36
+ index.catch(() => indexes.delete(root))
37
+ indexes.set(root, index)
38
+ }
39
+ const current = await index
40
+ await current.refresh()
41
+ return result(boundedCodeNavResult(current, input))
42
+ },
43
+ })
44
+ })
45
+ return () => { indexes.clear(); return registration.dispose() }
46
+ }
@@ -0,0 +1,488 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import type { Context } from "@opencode/plugin/promise/plugin"
3
+ import type { ToolContext } from "@opencode/plugin/promise/tool"
4
+ import type { Tool } from "@opencode/schema/tool"
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
6
+ import { basename, join, resolve } from "node:path"
7
+ import { DELIVERY_INVARIANTS, TEST_INFRASTRUCTURE_GUIDANCE, deliveryPhaseGuidance } from "../../../../lib/delivery-guidance.mjs"
8
+ import { deliveryRecovery, designReceipt, recoveryMessages } from "../../../../lib/delivery-context.mjs"
9
+ import { validatePlanning, inspectPlanning, recordPlanReview, approvePlanning, planningStatus, startImplementation } from "../../../../lib/planning.mjs"
10
+ import { PLANNING_START, planningGuide, planningNext } from "../../../../lib/planning-guide.mjs"
11
+ import { newDiscovery, restoreDiscovery, discoveryCheckpoint, discoveryEvent } from "../../../../lib/planning-discovery.mjs"
12
+ import { budgetLimits, newBudget, budgetReason, budgetSnapshot } from "../../../../lib/budget.mjs"
13
+ import { validatePlan, fingerprint, runCommand, pendingChecks, validateRevision, bindRequiredChecks, loadRequiredChecks, updateStepStatus, verificationMode, looksInformational, revisePlan, mergePlanPatch, completionIssues, planSteps, pendingRequirements, planD2WithProgress, evidenceIdentity } from "../../../../lib/delivery.mjs"
14
+ import { captureDeliveryReview, recordDeliveryReview } from "../../../../lib/delivery-review.mjs"
15
+ import { formatGuidance, loadGuidanceProfiles, routeGuidance } from "../../../../lib/guidance.mjs"
16
+ import { saveReport, reconcileReport } from "../../../../lib/reports.mjs"
17
+ import { lockWorkspace, selectReport, assertActiveReport } from "../../../../lib/workspace.mjs"
18
+ import { enforceToolPolicy, policyDescription } from "./policy.ts"
19
+ import { effectiveSessionOptions } from "../../../../lib/opencode/session-options.mjs"
20
+
21
+ /* eslint-disable @typescript-eslint/no-explicit-any -- Existing workflow snapshots mirror evolving JS domain schemas. */
22
+ type SessionState = { state: any; discovery: any; nudges: number; touched: boolean; budget: any; required: any[]; configError?: string; extraGuidance?: string; initialFiles: string[]; lastPrompt?: string; guidance?: string; writeCounts?: Record<string, number>; outcome?: string; cwd?: string }
23
+ type DeliveryOptions = Record<string, unknown>
24
+ type Schema = Record<string, unknown>
25
+ type DeliveryInput = Record<string, any>
26
+ type RegisteredToolContext = ToolContext
27
+ /* eslint-enable @typescript-eslint/no-explicit-any */
28
+
29
+ const S = (description?: string) => ({ type: "string", minLength: 1, maxLength: 1200, ...(description ? { description } : {}) })
30
+ const A = (items: Schema, maxItems = 20): Schema => ({ type: "array", items, maxItems })
31
+ const O = (properties: Record<string, Schema>, required = Object.keys(properties)): Schema => ({ type: "object", properties, required, additionalProperties: false })
32
+ const text = (value: unknown) => ({ content: typeof value === "string" ? value : JSON.stringify(value, null, 2) })
33
+ const empty = O({}, [])
34
+ const short = S()
35
+ const steps = A({ anyOf: [short, O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, title: short, kind: { type: "string", enum: ["work", "regression"] }, dependsOn: A(short, 40), checks: A(short, 24) }, ["id", "title"])] }, 40)
36
+ const checks = A(O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, kind: { type: "string", enum: ["test", "runtime", "static"] }, argv: A(short, 80), timeoutSeconds: { type: "integer", minimum: 1, maximum: 300 } }), 24)
37
+ const acceptance = A(O({ requirement: short, checks: A(short, 24) }), 40)
38
+ const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(short, 40), steps, verification: { type: "string", enum: ["required", "advisory", "none"] }, acceptance, checks, design: { type: "object", additionalProperties: true }, workflow: { type: "object", additionalProperties: true }, outcomes: A({ type: "object", additionalProperties: true }, 32) }, ["goal", "assumptions", "artifacts", "steps", "acceptance", "checks"])
39
+
40
+ function sessionKey(sessionID: string) { return `session/${sessionID}` }
41
+
42
+ /**
43
+ * Register CTG delivery tools and server hooks against OpenCode V2's Promise plugin context.
44
+ * Call from Plugin.define({ setup(ctx) { return registerDelivery(ctx, options) } }).
45
+ * Every operation derives both sessionID and worktree directory from the invocation/session API;
46
+ * no active-session process global is used. State is durable per session in ctx.storage while
47
+ * the existing .harness report and workspace lock remain canonical cross-process coordination.
48
+ */
49
+ export async function registerDelivery(ctx: Context, options: DeliveryOptions = {}) {
50
+ const registrations: Array<{ dispose(): Promise<void> }> = []
51
+ const serial = new Map<string, Promise<unknown>>()
52
+ const budgetTimers = new Map<string, ReturnType<typeof setTimeout>>()
53
+ const admittedCalls = new Set<string>()
54
+ const profiles = loadGuidanceProfiles()
55
+ let disposed = false
56
+ const limitOptions: Record<string, string> = { "delivery-max-tools": "maxTools", "delivery-max-seconds": "maxSeconds", "delivery-max-repairs": "maxRepairs" }
57
+ const sessionOptions = (session: Parameters<typeof effectiveSessionOptions>[0]): Record<string, unknown> => effectiveSessionOptions(session, { ...ctx.options, ...options })
58
+ async function scope(sessionID: string) {
59
+ if (!sessionID) throw Error("OpenCode sessionID is required")
60
+ const session = await ctx.session.get({ sessionID })
61
+ const cwd = session.location?.directory
62
+ if (typeof cwd !== "string" || !cwd) throw Error(`OpenCode session ${sessionID} has no worktree location`)
63
+ const opts = sessionOptions(session)
64
+ const saved = await ctx.storage.get(sessionKey(sessionID)) as SessionState | undefined
65
+ const data: SessionState = saved ? structuredClone(saved) : { state: null, discovery: newDiscovery(), nudges: 0, touched: false, budget: null, required: [], initialFiles: [] }
66
+ data.cwd = cwd
67
+ data.outcome = session.outcome
68
+ data.discovery = restoreDiscovery(data.discovery)
69
+ if (data.state) {
70
+ const dir = runDirectory(cwd, sessionID, data.state.runId)
71
+ data.state = reconcileReport(join(dir, "report.json"), data.state)
72
+ }
73
+ const limits = budgetLimits((name: string) => opts[limitOptions[name]] ?? opts[name] ?? "0")
74
+ if (!data.budget) data.budget = newBudget(limits)
75
+ else if (JSON.stringify(data.budget.limits) !== JSON.stringify(limits)) data.budget.limits = limits
76
+ if (data.budget.limits.seconds && !budgetTimers.has(sessionID) && !data.budget.stopped) {
77
+ const remaining = Math.max(1, data.budget.startedAt + data.budget.limits.seconds * 1000 - Date.now())
78
+ const timer = setTimeout(() => {
79
+ void serialized(sessionID, async () => {
80
+ const latest = await scope(sessionID)
81
+ if (!latest.data.budget.stopped) {
82
+ latest.data.budget.stopped = "elapsed-time"
83
+ await ctx.storage.set(sessionKey(sessionID), latest.data)
84
+ await ctx.session.interrupt({ sessionID, resume: false })
85
+ }
86
+ }).catch(error => console.error("Carthagent delivery budget stop failed", error))
87
+ }, remaining)
88
+ timer.unref?.(); budgetTimers.set(sessionID, timer)
89
+ }
90
+ if (!data.required.length) {
91
+ const explicit = typeof opts.validators === "string" ? opts.validators : undefined
92
+ const projectManifest = join(cwd, ".carthagent", "delivery.json")
93
+ const manifest = explicit ? resolve(cwd, explicit) : existsSync(projectManifest) ? projectManifest : undefined
94
+ try { if (manifest) data.required = loadRequiredChecks(manifest, cwd) }
95
+ catch (error) { data.configError = `Invalid required validators: ${error}` }
96
+ if (typeof opts.context === "string") {
97
+ try { data.extraGuidance = readFileSync(resolve(cwd, opts.context), "utf8") }
98
+ catch (error) { data.configError = `Invalid delivery context: ${error}` }
99
+ }
100
+ if (opts.protectExisting) data.initialFiles = readdirSync(cwd, { withFileTypes: true }).filter(e => e.isFile()).map(e => resolve(cwd, e.name))
101
+ }
102
+ if (data.state && data.required.length) {
103
+ for (const check of data.required) {
104
+ const old = data.state.plan.checks.find((c: { id: string }) => c.id === check.id)
105
+ if (JSON.stringify(old) !== JSON.stringify(check)) delete data.state.evidence[check.id]
106
+ }
107
+ data.state.plan = bindRequiredChecks(data.state.plan, data.required)
108
+ }
109
+ return { session, cwd, data, options: opts }
110
+ }
111
+ function runDirectory(cwd: string, sessionID: string, runId: string) { return join(cwd, ".harness", sessionID, runId) }
112
+ async function persist(cwd: string, sessionID: string, data: SessionState) {
113
+ if (data.state) {
114
+ const dir = runDirectory(cwd, sessionID, data.state.runId)
115
+ mkdirSync(dir, { recursive: true })
116
+ saveReport(join(dir, "report.json"), data.state)
117
+ selectReport(cwd, join(dir, "report.json"))
118
+ }
119
+ await ctx.storage.set(sessionKey(sessionID), data)
120
+ }
121
+ async function serialized<T>(sessionID: string, job: () => Promise<T>): Promise<T> {
122
+ const previous = serial.get(sessionID) || Promise.resolve()
123
+ const current = previous.then(job)
124
+ serial.set(sessionID, current.catch(() => {}))
125
+ return current
126
+ }
127
+ async function operate<T>(sessionID: string, job: (s: Awaited<ReturnType<typeof scope>>) => Promise<T>, replace = false): Promise<T> {
128
+ return serialized(sessionID, async () => {
129
+ const s = await scope(sessionID)
130
+ const release = lockWorkspace(s.cwd)
131
+ try {
132
+ if (!replace && s.data.state) {
133
+ const dir = runDirectory(s.cwd, sessionID, s.data.state.runId)
134
+ assertActiveReport(s.cwd, join(dir, "report.json"))
135
+ s.data.state = reconcileReport(join(dir, "report.json"), s.data.state)
136
+ }
137
+ return await job(s)
138
+ } finally { release() }
139
+ })
140
+ }
141
+ const add = (editor: Parameters<Parameters<Context["tool"]["transform"]>[0]>[0], name: string, description: string, input: Schema, execute: (input: DeliveryInput, toolContext: RegisteredToolContext) => Promise<{ content: string }>) => editor.add({
142
+ name, description, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
143
+ const value = rawValue as DeliveryInput
144
+ const { options: opts } = await scope(tool.sessionID)
145
+ if (!opts.delivery) throw Error("Carthagent delivery tools are disabled for this session.")
146
+ return execute(value, tool)
147
+ }, options: { namespace: "carthagent", codemode: true },
148
+ })
149
+
150
+ registrations.push(await ctx.tool.transform(editor => {
151
+ editor.namespace?.({ name: "carthagent", description: "Evidence-driven CTG delivery workflow tools" })
152
+ add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
153
+ if (data.configError) throw Error(data.configError)
154
+ const plan = validatePlan(bindRequiredChecks({ ...input, verification: input.verification ?? "required" }, data.required), cwd)
155
+ validateRevision(data.state, plan)
156
+ if (data.state && !["verified", "blocked"].includes(data.state.status)) data.state = revisePlan(data.state, plan, { cwd, hash: fingerprint(cwd, ["."]), reason: "Active plan replaced via delivery_plan" })
157
+ else data.state = { version: 1, workflowMode: opts.workflow === "strict" ? "strict" : "standard", reviewRequired: opts.workflow !== "strict", runId: randomUUID(), revision: (data.state?.revision || 0) + 1, plan, evidence: {}, status: "implementing", createdAt: new Date().toISOString(), stepStatus: {} }
158
+ data.discovery = newDiscovery(); const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
159
+ mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus))
160
+ await persist(cwd, tool.sessionID, data)
161
+ return text({ plan: join(dir, "plan.d2"), report: join(dir, "report.json"), next: data.state.workflowMode === "standard" ? "Contract accepted; implement, verify all checks, review and finish." : "No code or commands yet. Validate, inspect, adversarially review, then approve the design." })
162
+ }, true))
163
+ add(editor, "delivery_probe", "Bounded pre-plan version probe only; other APIs can be inspected with read.", O({ kind: { type: "string", enum: ["node-version", "npm-version"] } }), async (input, tool) => {
164
+ const { cwd } = await scope(tool.sessionID)
165
+ const argv = input.kind === "node-version" ? [process.execPath, "--version"] : [process.platform === "win32" ? "npm.cmd" : "npm", "--version"]
166
+ return text({ ...(await runCommand(argv, { cwd, signal: tool.signal, timeoutSeconds: 10 } as Parameters<typeof runCommand>[1])), evidence: false })
167
+ })
168
+ add(editor, "delivery_revise", "Revise the current task with a reason and partial patch. Existing obligations and verification history cannot be removed.", O({ reason: short, patch: { type: "object", additionalProperties: true } }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
169
+ if (!data.state) throw Error("Call delivery_plan first")
170
+ const hash = fingerprint(cwd, ["."])
171
+ data.state = revisePlan(data.state, bindRequiredChecks(mergePlanPatch(data.state.plan, input.patch), data.required), { cwd, reason: input.reason, hash })
172
+ await persist(cwd, tool.sessionID, data)
173
+ return text({ revision: data.state.revision, pendingChecks: pendingChecks(data.state, hash), unverifiedRequirements: pendingRequirements(data.state, hash), next: data.state.workflowMode === "standard" ? "Continue implementation; changed checks require fresh evidence." : "Revision relocks implementation; validate and re-approve." })
174
+ }))
175
+ add(editor, "delivery_design", "Strict workflow design gate. Guide, validate, inspect, adversarially review every scenario, then approve. Review is model-authored, not independent proof.", O({ action: { type: "string", enum: ["guide", "validate", "inspect", "review", "approve"] }, captureId: short, review: { type: "object", additionalProperties: true } }, ["action"]), async (input, tool) => {
176
+ if (input.action === "guide") return text(planningGuide())
177
+ return operate(tool.sessionID, async ({ cwd, data }) => {
178
+ if (!data.state) throw Error("Call delivery_plan first")
179
+ const hash = fingerprint(cwd, ["."]); let result
180
+ if (input.action === "validate") result = validatePlanning(data.state, hash)
181
+ else if (input.action === "inspect") result = inspectPlanning(data.state, hash)
182
+ else if (input.action === "review") result = recordPlanReview(data.state, hash, { ...input.review, captureId: input.review?.captureId ?? input.captureId })
183
+ else if (input.action === "approve") result = approvePlanning(data.state, hash)
184
+ else throw Error("Unknown delivery_design action")
185
+ await persist(cwd, tool.sessionID, data)
186
+ return text({ result: designReceipt(input.action, result), ...(input.action === "inspect" ? { captureId: result.id } : {}), ...(input.action === "validate" ? planningNext(data.state, result) : {}), planningStatus: planningStatus(data.state, hash) })
187
+ })
188
+ })
189
+ add(editor, "delivery_status", "Show delivery state, stale/failed checks and next action. Call finish instead of polling when readyToFinish is true.", empty, async (_input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
190
+ if (!data.state) return text("No delivery contract.")
191
+ const dir = runDirectory(cwd, tool.sessionID, data.state.runId); const hash = fingerprint(cwd, ["."])
192
+ const issues = completionIssues(data.state, cwd, hash); const readyToFinish = Object.values(issues).every(items => !items.length)
193
+ return text({ ...deliveryRecovery(data.state, { report: join(dir, "report.json") }), pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish, next: readyToFinish ? "Call delivery_finish; do not poll again." : "Run declared checks, complete steps/outputs, and resolve outstanding requirements." })
194
+ }))
195
+ add(editor, "delivery_progress", "Update implementation step progress and regenerate the plan diagram.", O({ step: { anyOf: [{ type: "integer", minimum: 0 }, short] }, status: { type: "string", enum: ["active", "done", "failed", "pending"] }, note: short }, ["step", "status"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
196
+ if (!data.state) throw Error("Call delivery_plan first")
197
+ const step = planSteps(data.state.plan).find((s: { id: string }, i: number) => typeof input.step === "string" ? s.id === input.step : i === input.step)
198
+ const hash = input.status === "done" && step?.checks.length ? fingerprint(cwd, ["."]) : undefined
199
+ updateStepStatus(data.state, input.step, input.status, hash)
200
+ if (input.note) data.state.progressNotes = [...(data.state.progressNotes || []), { step: step?.id, status: input.status, note: input.note, revision: data.state.revision, at: new Date().toISOString() }]
201
+ const dir = runDirectory(cwd, tool.sessionID, data.state.runId); writeFileSync(join(dir, "plan.d2"), planD2WithProgress(data.state.plan, data.state.stepStatus)); await persist(cwd, tool.sessionID, data)
202
+ return text({ step: input.step, status: input.status, plan: join(dir, "plan.d2") })
203
+ }))
204
+ add(editor, "delivery_check", "Run declared check(s) with no implicit shell, deadline, full logs and current workspace fingerprint. Checks never fabricate evidence.", O({ id: short }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
205
+ if (!data.state) throw Error("Call delivery_plan first")
206
+ const verification = verificationMode(data.state.plan)
207
+ if (verification === "none") return text({ verification, note: "No checks are declared for this informational plan; call delivery_finish." })
208
+ startImplementation(data.state, fingerprint(cwd, ["."]))
209
+ const selected = input.id === "all" ? data.state.plan.checks : data.state.plan.checks.filter((c: { id: string }) => c.id === input.id)
210
+ if (!selected.length) throw Error("Unknown check ID; use delivery_status or id=all")
211
+ const results = []
212
+ for (const check of selected) {
213
+ if (tool.signal.aborted) throw Error("Check cancelled before execution")
214
+ const identity = evidenceIdentity(data.state, check); data.state.status = "verifying"; data.state.verificationStarted = true; delete data.state.evidence[check.id]
215
+ const before = fingerprint(cwd, ["."]); await persist(cwd, tool.sessionID, data)
216
+ const dir = runDirectory(cwd, tool.sessionID, data.state.runId)
217
+ const result = await runCommand(check.argv, { cwd, timeoutSeconds: check.timeoutSeconds, signal: tool.signal, logPath: join(dir, `${check.id}-${randomUUID()}.log`) } as Parameters<typeof runCommand>[1])
218
+ const after = fingerprint(cwd, ["."]); const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after
219
+ data.state.evidence[check.id] = { passed, ...identity, executedRevision: data.state.revision, fingerprint: after, code: result.code, timedOut: result.timedOut, cancelled: result.cancelled, logPath: result.logPath, durationMs: result.durationMs, changedDuringCheck: before !== after, outputTail: result.output.slice(-1200) }
220
+ results.push({ id: check.id, advisory: verification !== "required", ...data.state.evidence[check.id], outputTail: result.output })
221
+ await persist(cwd, tool.sessionID, data)
222
+ }
223
+ const failures = results.filter(r => !r.passed && verification === "required")
224
+ if (failures.length) throw Error(JSON.stringify({ failures, next: "Repair actual failures; do not weaken obligations." }))
225
+ const hash = fingerprint(cwd, ["."]); const issues = completionIssues(data.state, cwd, hash)
226
+ return text({ results, pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish: Object.values(issues).every(items => !items.length) })
227
+ }))
228
+ add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
229
+ if (!data.state) throw Error("Call delivery_plan first")
230
+ const result = input.action === "inspect" ? await captureDeliveryReview(data.state, cwd, runDirectory(cwd, tool.sessionID, data.state.runId), tool.signal) : input.action === "record" ? recordDeliveryReview(data.state, cwd, input) : (() => { throw Error("Review action must be inspect or record") })()
231
+ await persist(cwd, tool.sessionID, data); return text(result)
232
+ }))
233
+ add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
234
+ if (!data.state) throw Error("Call delivery_plan first")
235
+ if (data.configError) throw Error(data.configError)
236
+ const verification = verificationMode(data.state.plan); let hash: string | null = null
237
+ if (input.status === "verified") {
238
+ hash = fingerprint(cwd, ["."])
239
+ if (verification === "required") {
240
+ const issues = completionIssues(data.state, cwd, hash)
241
+ if (Object.values(issues).some(items => items.length)) throw Error(`Cannot verify: ${JSON.stringify(issues)}`)
242
+ }
243
+ } else {
244
+ if (!input.limitations.length) throw Error("Blocked delivery requires an explicit limitation/reason")
245
+ try { hash = fingerprint(cwd, ["."]) } catch { hash = "unfingerprinted:scope_exceeded" }
246
+ }
247
+ data.state.status = input.status; data.state.handoff = { ...input, verification, advisory: verification !== "required", fingerprint: hash, at: new Date().toISOString(), evidence: structuredClone(data.state.evidence), reviewEvidence: data.state.reviewEvidence || null }
248
+ await persist(cwd, tool.sessionID, data); return text({ status: data.state.status, verification, report: join(runDirectory(cwd, tool.sessionID, data.state.runId), "report.json") })
249
+ }))
250
+ }))
251
+
252
+ registrations.push(await ctx.tool.hook("execute.before", async event => serialized(event.sessionID, async () => {
253
+ if (typeof event.id !== "string" || !event.id) throw Error("Tool execution locked: host did not provide an authoritative call ID.")
254
+ if (typeof event.messageID !== "string" || !event.messageID) throw Error("Tool execution locked: host did not provide an authoritative message ID.")
255
+ // Include tool name because Code Mode's dispatcher and its nested tools can
256
+ // share the outer call ID/message ID while still being distinct invocations.
257
+ const callKey = `${event.sessionID}\0${event.messageID}\0${event.id}\0${event.tool}`
258
+ if (admittedCalls.has(callKey)) return
259
+ const { cwd, data, options: opts } = await scope(event.sessionID)
260
+ if (!opts.delivery && Number(data.budget.limits.tools) === 0 && Number(data.budget.limits.seconds) === 0 && Number(data.budget.limits.repairs) === 0) return
261
+ let persistenceAttempted = false
262
+ try {
263
+ const nativeShell = ["bash", "shell", "powershell"].includes(event.tool)
264
+ const input = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : {}
265
+ if (opts.delivery && event.tool !== "execute" && requiresImplementationTool(event.tool)) {
266
+ if (!data.state) throw Error(`Implementation locked: call delivery_plan first (tool ${event.tool}).`)
267
+ const release = lockWorkspace(cwd)
268
+ try {
269
+ const dir = runDirectory(cwd, event.sessionID, data.state.runId)
270
+ assertActiveReport(cwd, join(dir, "report.json"))
271
+ data.state = reconcileReport(join(dir, "report.json"), data.state)
272
+ enforceToolPolicy(event.tool, data.state, cwd)
273
+ await persist(cwd, event.sessionID, data)
274
+ } finally { release() }
275
+ }
276
+ if ((event.tool === "write" || event.tool === "edit") && typeof input.path === "string") {
277
+ const path = input.path.replaceAll("\\", "/"), cwdPath = cwd.replaceAll("\\", "/").replace(/^\/+/, ""), name = basename(cwd)
278
+ if ([cwdPath, name].some(prefix => path === prefix || path.startsWith(`${prefix}/`))) throw Error("Path recreates working directory inside itself; use a workspace-relative product path.")
279
+ if (event.tool === "write" && data.initialFiles.includes(resolve(cwd, input.path))) throw Error(`Preserve existing ${input.path}; use focused edits.`)
280
+ }
281
+ const cap = Number(opts.bashCap) || 0
282
+ if (nativeShell) {
283
+ const command = String(input.command ?? "")
284
+ if (/\b(?:pkill|killall)\b|\bkill\s+(?:-\S+\s+)*(?:-1|0)(?:\s|$)/i.test(command)) throw Error("Broad process termination is prohibited; terminate only a captured child PID.")
285
+ if (cap > 0 && (typeof input.timeout !== "number" || input.timeout > cap)) input.timeout = cap
286
+ }
287
+ if (["write", "edit", "patch"].includes(event.tool) && opts.rewriteCap && Number(opts.rewriteCap) > 0) {
288
+ data.writeCounts ||= {}
289
+ const path = String(input.path || "")
290
+ const count = data.writeCounts[path] || 0
291
+ if (count >= Number(opts.rewriteCap)) throw Error(`Full-file rewrite limit reached for ${path}; preserve behavior and use focused edits.`)
292
+ data.writeCounts[path] = count + 1
293
+ }
294
+ // execute.before is the authoritative per-invocation boundary, including
295
+ // native shell, plugin tools, subagent dispatch, and Code Mode calls. It
296
+ // carries sessionID/call ID; shell.create.before does not.
297
+ if (!admittedCalls.has(callKey)) {
298
+ const reason = budgetReason(data.budget, "tool")
299
+ if (reason) {
300
+ data.budget.stopped ||= reason
301
+ persistenceAttempted = true
302
+ await ctx.storage.set(sessionKey(event.sessionID), data)
303
+ await ctx.session.interrupt({ sessionID: event.sessionID, resume: false })
304
+ throw Error(`Delivery budget exhausted: ${reason}. Explicit delivery-budget-reset required.`)
305
+ }
306
+ data.budget.tools++
307
+ }
308
+ persistenceAttempted = true
309
+ await ctx.storage.set(sessionKey(event.sessionID), data)
310
+ admittedCalls.add(callKey)
311
+ } catch (error) {
312
+ // A policy rejection means no tool executes. Persist the observed state but
313
+ // never leave a provisional shell claim behind for a later user command.
314
+ if (!persistenceAttempted) await ctx.storage.set(sessionKey(event.sessionID), data)
315
+ throw error
316
+ }
317
+ })))
318
+
319
+ registrations.push(await ctx.tool.hook("execute.after", async event => serialized(event.sessionID, async () => {
320
+ const { cwd, data } = await scope(event.sessionID)
321
+ if (!sessionOptions(await ctx.session.get({ sessionID: event.sessionID })).delivery) {
322
+ await ctx.storage.set(sessionKey(event.sessionID), data)
323
+ return
324
+ }
325
+ const failed = event.status === "error"
326
+ if (!failed && ["write", "edit"].includes(event.tool)) data.touched = true
327
+ if (data.discovery.active && ["read", "ls", "find", "grep", "code_nav"].includes(event.tool)) data.discovery = discoveryEvent(data.discovery, "read")
328
+ if (data.state && ["delivery_check", "delivery_progress", "delivery_review", "delivery_finish", "delivery_revise", "delivery_design", "delivery_plan"].includes(event.tool)) {
329
+ const dir = runDirectory(cwd, event.sessionID, data.state.runId)
330
+ if (existsSync(join(dir, "report.json"))) data.state = reconcileReport(join(dir, "report.json"), data.state)
331
+ }
332
+ const { options: opts } = await scope(event.sessionID)
333
+ const outputCap = Math.max(0, Number(opts.toolOutputCap) || 0)
334
+ if (!failed && outputCap > 0 && event.result?.content) {
335
+ if (!Array.isArray(event.result.content)) {
336
+ await ctx.storage.set(sessionKey(event.sessionID), data)
337
+ return
338
+ }
339
+ const content = event.result.content.map(item => {
340
+ if (item.type !== "text" || item.text.length <= outputCap) return item
341
+ const marker = `\n...[${item.text.length - outputCap} chars omitted; rerun a narrower command]...\n`
342
+ const head = Math.floor(Math.max(0, outputCap - marker.length) / 3)
343
+ const tail = Math.max(0, outputCap - marker.length - head)
344
+ return { ...item, text: outputCap <= marker.length ? item.text.slice(0, outputCap) : item.text.slice(0, head) + marker + item.text.slice(-tail) }
345
+ })
346
+ event.result = { ...event.result, content }
347
+ }
348
+ await ctx.storage.set(sessionKey(event.sessionID), data)
349
+ })))
350
+
351
+ registrations.push(await ctx.shell.hook("create.before", async event => {
352
+ // Session-scoped policy, limits, and budget were handled at execute.before.
353
+ // This hook intentionally does not attribute commands: 2.0.32 has no
354
+ // authoritative source.sessionID/callID on ShellCreateBefore.
355
+ const cap = Number(options.bashCap) || 0
356
+ if (cap > 0 && event.timeout > cap) event.timeout = cap
357
+ if (/\b(?:pkill|killall)\b|\bkill\s+(?:-\S+\s+)*(?:-1|0)(?:\s|$)/i.test(event.command)) throw Error("Broad process termination is prohibited; terminate only a captured child PID.")
358
+ }))
359
+
360
+ for (const kind of ["context", "compaction"] as const) registrations.push(await ctx.session.hook(kind, async event => {
361
+ const { cwd, data } = await scope(event.sessionID)
362
+ if (!sessionOptions(await ctx.session.get({ sessionID: event.sessionID })).delivery) return
363
+ // Prevent bypass through built-in shell/edit, plugin and child-session tools,
364
+ // and Code Mode while the participating-tool policy is locked. Runtime hooks
365
+ // still repeat the check at invocation time; hiding a model tool is not the
366
+ // authorization boundary by itself.
367
+ const locked = !data.state || planningStatus(data.state, fingerprint(cwd, ["."])).locked
368
+ if (locked && event.tools && typeof event.tools === "object") {
369
+ for (const name of Object.keys(event.tools)) if (requiresImplementationTool(name)) delete event.tools[name]
370
+ }
371
+ if (!data.state || ["verified", "blocked"].includes(data.state.status)) {
372
+ const checkpoint = discoveryCheckpoint(data.discovery)
373
+ if (checkpoint) event.system.push({ type: "text", text: checkpoint })
374
+ return
375
+ }
376
+ const dir = runDirectory(cwd, event.sessionID, data.state.runId)
377
+ const recovery = deliveryRecovery(data.state, { report: join(dir, "report.json"), planningStatus: planningStatus(data.state, fingerprint(cwd, ["."])) })
378
+ const checkpoint = recoveryMessages([], recovery).map((message: { content: string }) => message.content).join("\n")
379
+ event.system.push({ type: "text", text: `${data.guidance || DELIVERY_INVARIANTS}\n\nCurrent delivery recovery record (not authorization; recompute freshness before checks/finish):\n${checkpoint}\n\n${deliveryPhaseGuidance(data.state)}` })
380
+ }))
381
+
382
+ registrations.push(await ctx.session.hook("prompt", async event => serialized(event.sessionID, async () => {
383
+ const { cwd, data, options: opts } = await scope(event.sessionID)
384
+ if (!opts.delivery) return
385
+ const workflow = data.state && !["verified", "blocked"].includes(data.state.status) ? data.state.workflowMode : opts.workflow
386
+ const routed = routeGuidance(event.prompt.text, { cwd, profiles })
387
+ let guidance = DELIVERY_INVARIANTS + "\n" + deliveryPhaseGuidance(data.state) + "\n" + policyDescription(workflow)
388
+ if (workflow === "standard") guidance += "\nA valid required plan allows implementation; final evidence and review stay mandatory."
389
+ if (/\b(test|typescript|route|backend|api)\b/i.test(event.prompt.text)) guidance += `\n${TEST_INFRASTRUCTURE_GUIDANCE}`
390
+ if (data.discovery.active && workflow === "strict") guidance += `\n${PLANNING_START}`
391
+ const routedText = formatGuidance(routed); if (routedText) guidance += `\n${routedText}`
392
+ if (data.extraGuidance) guidance += `\nTask-specific delivery context:\n${data.extraGuidance}`
393
+ if (data.required.length) guidance += `\nUser-owned required validators are enforced: ${JSON.stringify(data.required)}. Run every required check; do not bypass.`
394
+ if (looksInformational(event.prompt.text)) guidance += `\nReceipt check: informational/read-only ask; answer directly or use verification none/advisory. Do not enter a repair loop.`
395
+ data.lastPrompt = event.prompt.text
396
+ data.guidance = `${guidance}\n\n${data.state ? deliveryRecovery(data.state, { report: join(runDirectory(cwd, event.sessionID, data.state.runId), "report.json") }) ? "Existing delivery state remains authoritative; do not trust summary claims over current report/fingerprint." : "" : ""}`
397
+ const internal = event.metadata?.carthagentRecovery === true || event.metadata?.carthagentBudget === true
398
+ if (!internal) { data.discovery = newDiscovery(!looksInformational(event.prompt.text)); data.nudges = 0; data.touched = false }
399
+ await ctx.storage.set(sessionKey(event.sessionID), data)
400
+ })))
401
+
402
+ registrations.push(await ctx.command.transform(editor => {
403
+ editor.add({ name: "delivery-budget-status", description: "Show this session's persistent delivery budget", execute: async ({ sessionID }) => {
404
+ const { data } = await scope(sessionID)
405
+ await ctx.session.prompt({ sessionID, delivery: "steer", metadata: { carthagentBudget: true }, text: `Delivery budget status: ${JSON.stringify(budgetSnapshot(data.budget))}` })
406
+ } })
407
+ editor.add({ name: "delivery-budget-reset", description: "Explicitly authorize a fresh budget for this session", execute: async ({ sessionID }) => {
408
+ const snapshot = await serialized(sessionID, async () => {
409
+ const { data, options: opts } = await scope(sessionID); clearTimeout(budgetTimers.get(sessionID)); budgetTimers.delete(sessionID); data.budget = newBudget(budgetLimits((name: string) => opts[limitOptions[name]] ?? opts[name] ?? "0")); await ctx.storage.set(sessionKey(sessionID), data)
410
+ for (const key of admittedCalls) if (key.startsWith(`${sessionID}\0`)) admittedCalls.delete(key)
411
+ return budgetSnapshot(data.budget)
412
+ })
413
+ await ctx.session.prompt({ sessionID, delivery: "steer", metadata: { carthagentBudget: true }, text: `Delivery budget explicitly reset: ${JSON.stringify(snapshot)}` })
414
+ } })
415
+ }))
416
+
417
+ const abort = new AbortController()
418
+ void (async () => {
419
+ try {
420
+ for await (const event of ctx.event.subscribe({ signal: abort.signal })) {
421
+ if (event.type === "session.idle" && event.data?.sessionID) {
422
+ const sessionID = event.data.sessionID
423
+ let followup: string | undefined
424
+ await serialized(sessionID, async () => {
425
+ const { cwd, data, session } = await scope(sessionID)
426
+ const reason = budgetReason(data.budget)
427
+ if (reason && !data.budget.stopped) {
428
+ data.budget.stopped = reason
429
+ await ctx.storage.set(sessionKey(sessionID), data)
430
+ await ctx.session.interrupt({ sessionID, resume: false })
431
+ return
432
+ }
433
+ if (data.discovery.active) data.discovery = discoveryEvent(data.discovery, "blocked")
434
+ const state = data.state
435
+ if (!state || data.nudges >= 2 || ["verified", "blocked"].includes(state.status) || !data.touched || verificationMode(state.plan) !== "required" || session.outcome !== "succeeded") {
436
+ await ctx.storage.set(sessionKey(sessionID), data)
437
+ return
438
+ }
439
+ const release = lockWorkspace(cwd)
440
+ try {
441
+ const dir = runDirectory(cwd, sessionID, state.runId)
442
+ assertActiveReport(cwd, join(dir, "report.json"))
443
+ data.state = reconcileReport(join(dir, "report.json"), state)
444
+ const hash = fingerprint(cwd, ["."])
445
+ const issues = completionIssues(data.state, cwd, hash)
446
+ const fresh = data.state.handoff?.fingerprint === hash
447
+ if (fresh || !Object.values(issues).some(items => items.length)) {
448
+ await ctx.storage.set(sessionKey(sessionID), data)
449
+ return
450
+ }
451
+ const repairReason = budgetReason(data.budget, "repair")
452
+ if (repairReason) {
453
+ data.budget.stopped = repairReason
454
+ await ctx.storage.set(sessionKey(sessionID), data)
455
+ await ctx.session.interrupt({ sessionID, resume: false })
456
+ return
457
+ }
458
+ data.budget.repairs++
459
+ data.nudges++
460
+ const pending = issues.pendingChecks.join(", ") || "none"
461
+ await ctx.storage.set(sessionKey(sessionID), data)
462
+ followup = `Delivery follow-up ${data.nudges}/2: the required contract has no fresh verified handoff. Pending checks: ${pending}. Inspect current delivery_status, repair the actual cause, rerun checks and finish verified, or record status=blocked with specific limitations. Do not repeat a success claim or weaken obligations.`
463
+ } catch (error) {
464
+ console.error("Carthagent delivery recovery failed closed", error)
465
+ } finally { release() }
466
+ })
467
+ if (followup) await ctx.session.prompt({ sessionID, metadata: { carthagentRecovery: true }, text: followup, delivery: "steer" })
468
+ for (const key of admittedCalls) if (key.startsWith(`${sessionID}\0`)) admittedCalls.delete(key)
469
+ }
470
+ }
471
+ } catch (error) { if (!abort.signal.aborted) console.error("Carthagent delivery event subscription failed", error) }
472
+ })()
473
+ return async () => {
474
+ if (disposed) return
475
+ disposed = true; abort.abort()
476
+ admittedCalls.clear()
477
+ for (const timer of budgetTimers.values()) clearTimeout(timer)
478
+ budgetTimers.clear()
479
+ await Promise.all(registrations.map(registration => registration.dispose()))
480
+ serial.clear()
481
+ }
482
+ }
483
+
484
+ function requiresImplementationTool(name: string) {
485
+ // V2 exposes namespaced tools as `namespace_name` in model context and hooks.
486
+ const toolName = name.startsWith("carthagent_") ? name.slice("carthagent_".length) : name
487
+ return !["read", "ls", "find", "grep", "execute", "code_nav", "delivery_probe", "delivery_plan", "delivery_revise", "delivery_design", "delivery_status", "delivery_finish", "delivery-budget-status", "delivery-budget-reset"].includes(toolName)
488
+ }
@@ -0,0 +1,91 @@
1
+ import type { Credential } from '@opencode/schema/credential';
2
+ import { Integration as IntegrationSchema } from '@opencode/schema/integration';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { CloudClient, cloudControlUrl, oauthCredentials } from '../../../../lib/cloud/client.mjs';
5
+ import { shipInstallId } from '../../../../lib/ship-telemetry.mjs';
6
+
7
+ const METHOD_ID = 'carthagent-device';
8
+ const MAX_EXPIRY_MS = 15 * 60_000;
9
+
10
+ type Flow = {
11
+ controller: AbortController;
12
+ authorization: Awaited<ReturnType<CloudClient['createDeviceAuthorization']>>;
13
+ callback: Promise<Credential.OAuth>;
14
+ expiresAt: number;
15
+ timer: ReturnType<typeof setTimeout>;
16
+ };
17
+
18
+ export function createDeviceAuthController({ env = process.env, fetchImpl = globalThis.fetch, agentDir, now = Date.now, sleep = (ms: number, signal?: AbortSignal) => new Promise<void>((resolve, reject) => {
19
+ if (signal?.aborted) return reject(signal.reason);
20
+ const timer = setTimeout(resolve, ms);
21
+ signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }, { once: true });
22
+ }) }: {
23
+ env?: NodeJS.ProcessEnv; fetchImpl?: typeof fetch; agentDir: string; now?: () => number;
24
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
25
+ }) {
26
+ const client = new CloudClient({ baseUrl: cloudControlUrl(env), fetchImpl, sleep: ms => sleep(ms) });
27
+ const flows = new Map<string, Flow>();
28
+ const remove = (id: string) => {
29
+ const flow = flows.get(id);
30
+ if (!flow) return false;
31
+ clearTimeout(flow.timer);
32
+ flows.delete(id);
33
+ return true;
34
+ };
35
+ const start = async (signal?: AbortSignal) => {
36
+ // Bound active generations; expired callbacks are aborted by their timer.
37
+ for (const [id, flow] of flows) if (flow.expiresAt <= now()) { flow.controller.abort(new Error('expired_token')); remove(id); }
38
+ if (flows.size >= 4) throw new Error('too_many_auth_flows');
39
+ const request = client.request as unknown as (path: string, options: { method: string; body: unknown; signal?: AbortSignal }) => Promise<{
40
+ userCode: string; verificationUri: string; verificationUriComplete?: string; expiresIn: number; interval: number; deviceCode: string;
41
+ }>;
42
+ const createAuthorization = (input: { clientName: string; installId: string }, signal?: AbortSignal) => request.call(client, '/v1/device/authorizations', {
43
+ method: 'POST', body: input, signal,
44
+ });
45
+ const authorization = await createAuthorization({
46
+ clientName: `Carthagent OpenCode (${process.env.HOSTNAME || 'device'})`,
47
+ installId: shipInstallId({ agentDir }),
48
+ }, signal);
49
+ const flowId = randomUUID();
50
+ const controller = new AbortController();
51
+ const expiresAt = now() + Math.min(MAX_EXPIRY_MS, Math.max(1, Number(authorization.expiresIn) || 600) * 1000);
52
+ const timer = setTimeout(() => controller.abort(new Error('expired_token')), Math.max(1, expiresAt - now()));
53
+ const flow: Flow = { controller, authorization, callback: Promise.resolve({} as Credential.OAuth), expiresAt, timer };
54
+ flows.set(flowId, flow);
55
+ const callback = (async () => {
56
+ try {
57
+ const interval = Math.max(1000, Number(authorization.interval || 5) * 1000);
58
+ while (now() < expiresAt) {
59
+ controller.signal.throwIfAborted();
60
+ try {
61
+ const tokens = await client.pollDeviceToken({ deviceCode: authorization.deviceCode, signal: controller.signal });
62
+ return { type: 'oauth', methodID: IntegrationSchema.MethodID.make(METHOD_ID), ...oauthCredentials(tokens, now()) } as Credential.OAuth;
63
+ } catch (error) {
64
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'authorization_pending') throw error;
65
+ }
66
+ await sleep(interval, controller.signal);
67
+ }
68
+ throw new Error('expired_token');
69
+ } finally { remove(flowId); }
70
+ })();
71
+ // Avoid unhandled rejection for RPC-only starts; native authorize still
72
+ // awaits this same promise and receives the original rejection.
73
+ void callback.catch(() => {});
74
+ flow.callback = callback;
75
+ return { flowId, authorization, callback, expiresAt };
76
+ };
77
+ return {
78
+ start,
79
+ cancel(flowId: string) {
80
+ const flow = flows.get(flowId);
81
+ if (!flow) return false;
82
+ flow.controller.abort(new Error('cancelled'));
83
+ return remove(flowId);
84
+ },
85
+ async cleanup() {
86
+ for (const flow of flows.values()) { clearTimeout(flow.timer); flow.controller.abort(new Error('plugin_unloaded')); }
87
+ flows.clear();
88
+ await Promise.resolve();
89
+ },
90
+ };
91
+ }