carthagent 1.0.7 → 1.1.0-beta.1

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 (294) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/beta-rewards-e2e-plan.md +48 -0
  4. package/docs/beta-rewards-e2e-results.md +84 -0
  5. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  6. package/docs/glyph-review/test-isolation-v1.md +26 -0
  7. package/docs/glyph-review/typecheck-base.log +1 -0
  8. package/docs/glyph-review/typecheck-v4.log +1 -0
  9. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  10. package/docs/help-dispatch-review.md +69 -0
  11. package/docs/launch-readiness-live-results.md +65 -0
  12. package/docs/legacy-cli.md +150 -0
  13. package/docs/live-theme-plugin-api.md +18 -0
  14. package/docs/native-cli.md +102 -0
  15. package/docs/native-compatibility.md +16 -0
  16. package/docs/native-daily-quests.md +132 -0
  17. package/docs/native-glyph-pty.md +59 -0
  18. package/docs/native-manual-testing.md +336 -0
  19. package/docs/native-slash-acceptance-handoff.md +49 -0
  20. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  21. package/docs/native-terminal-performance.md +310 -0
  22. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  23. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  24. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  35. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  36. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  37. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  38. package/docs/native-terminal-semantics.md +89 -0
  39. package/docs/native-upgrade.md +175 -0
  40. package/docs/opencode-composed-source-review.md +89 -0
  41. package/docs/opencode-migration.md +225 -9
  42. package/docs/opencode-owned-runtime.md +241 -0
  43. package/docs/opencode-runtime.md +12 -0
  44. package/docs/opencode-theme-provider-proof/README.md +55 -0
  45. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  46. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  47. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  48. package/docs/owned-host-controls-proof.md +39 -0
  49. package/docs/owned-runtime-release-manifest.md +42 -0
  50. package/docs/owned-visual-clear-proof.md +40 -0
  51. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  52. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  53. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  54. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  61. package/docs/release-notes-beta.1.md +60 -0
  62. package/docs/release-notes-native-draft.md +73 -0
  63. package/docs/release-notes-preview.10.md +121 -0
  64. package/docs/release-notes-preview.11.md +76 -0
  65. package/docs/release-notes-preview.12.md +55 -0
  66. package/docs/release-notes-preview.13.md +68 -0
  67. package/docs/release-notes-preview.9.md +130 -0
  68. package/docs/source-preview.md +120 -0
  69. package/evaluate.mjs +28 -1
  70. package/lib/budget.mjs +1 -1
  71. package/lib/codegraph.mjs +1 -1
  72. package/lib/delivery-review.mjs +19 -6
  73. package/lib/delivery.mjs +3 -2
  74. package/lib/environment.mjs +8 -0
  75. package/lib/guidance.mjs +1 -1
  76. package/lib/opencode/cli-runtime.mjs +1171 -0
  77. package/lib/opencode/cli.mjs +252 -0
  78. package/lib/opencode/credentials.mjs +63 -8
  79. package/lib/opencode/demo.mjs +53 -0
  80. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  81. package/lib/opencode/evaluation.mjs +32 -12
  82. package/lib/opencode/headless.mjs +22 -5
  83. package/lib/opencode/provider-policy.mjs +23 -0
  84. package/lib/opencode/review.mjs +37 -16
  85. package/lib/opencode/runtime-artifact.mjs +335 -0
  86. package/lib/opencode/runtime-github-transport.mjs +121 -0
  87. package/lib/opencode/runtime-release.json +36 -0
  88. package/lib/opencode/runtime.mjs +299 -20
  89. package/lib/opencode/service.mjs +77 -35
  90. package/lib/opencode/session-options.mjs +6 -2
  91. package/lib/opencode/update.mjs +222 -0
  92. package/lib/opencode/workspace.mjs +1 -1
  93. package/lib/planning-advice.mjs +2 -2
  94. package/lib/providers/experiential.mjs +2 -2
  95. package/lib/tui/framing.mjs +1 -80
  96. package/lib/update.mjs +10 -1
  97. package/package.json +33 -7
  98. package/plugins/carthagent/README.md +23 -0
  99. package/plugins/carthagent/src/bootstrap.ts +1 -1
  100. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  101. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  102. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  103. package/plugins/carthagent/src/index.ts +71 -24
  104. package/plugins/carthagent/src/rpc.ts +51 -6
  105. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  106. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  107. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  108. package/plugins/carthagent/src/server/delivery.ts +227 -112
  109. package/plugins/carthagent/src/server/framing.mjs +80 -0
  110. package/plugins/carthagent/src/server/policy.ts +1 -1
  111. package/plugins/carthagent/src/server/providers.ts +59 -31
  112. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  113. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  114. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  115. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  116. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  117. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  118. package/plugins/carthagent/src/testing.ts +5 -0
  119. package/plugins/carthagent/src/tui.tsx +496 -0
  120. package/plugins/carthagent/src/ui/client.ts +127 -0
  121. package/plugins/carthagent/src/ui/commands.ts +70 -0
  122. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  123. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  124. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  125. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  126. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  127. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  128. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  129. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  130. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  131. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  132. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  133. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  134. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  135. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  136. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  137. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  138. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  139. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  140. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  141. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  142. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  143. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  144. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  145. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  146. package/plugins/carthagent/tests/commands.test.ts +107 -0
  147. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  148. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  149. package/plugins/carthagent/themes/carthage.json +67 -0
  150. package/plugins/carthagent/tui.ts +26 -1
  151. package/script/ctg/build-owned-runtime.sh +166 -0
  152. package/tests/budget-engine.test.mjs +4 -3
  153. package/tests/cli.test.mjs +52 -96
  154. package/tests/daily-evidence.test.mjs +62 -0
  155. package/tests/delivery-review.test.mjs +57 -2
  156. package/tests/delivery.test.mjs +22 -1
  157. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  158. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  159. package/tests/installed-runtime-contract.test.mjs +43 -0
  160. package/tests/native-cli-host-smoke.mjs +457 -0
  161. package/tests/native-daily-editor-focus.test.mjs +213 -0
  162. package/tests/native-daily-editor.test.mjs +43 -0
  163. package/tests/native-daily-quests-layout.test.mjs +55 -0
  164. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  165. package/tests/native-daily-quests-ui.test.mjs +778 -0
  166. package/tests/native-daily-source.test.mjs +525 -0
  167. package/tests/native-demo-ui.test.mjs +48 -0
  168. package/tests/native-evaluation-admission.test.mjs +24 -0
  169. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  170. package/tests/native-fixture-runtime.test.mjs +111 -0
  171. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  172. package/tests/native-glyph-pty.test.mjs +107 -0
  173. package/tests/native-manual-projects.test.mjs +170 -0
  174. package/tests/native-model-command.test.mjs +67 -0
  175. package/tests/native-owned-runtime-env.test.mjs +140 -0
  176. package/tests/native-paired-guards.test.mjs +464 -0
  177. package/tests/native-provider-policy.test.mjs +31 -0
  178. package/tests/native-pty-invocation.test.mjs +114 -0
  179. package/tests/native-pty-runtime.test.mjs +45 -0
  180. package/tests/native-resume-session.test.mjs +46 -0
  181. package/tests/native-review-consumer.test.mjs +286 -0
  182. package/tests/native-runtime-identity.test.mjs +214 -0
  183. package/tests/native-select-mouse.test.mjs +23 -0
  184. package/tests/native-ship-guidance.test.mjs +16 -0
  185. package/tests/native-ship-parity.test.mjs +526 -0
  186. package/tests/native-ship-plan.test.mjs +85 -0
  187. package/tests/native-ship-rpc.test.mjs +71 -0
  188. package/tests/native-ui-commands.test.mjs +6 -0
  189. package/tests/native-voyages-layout.test.mjs +28 -0
  190. package/tests/native-voyages-navigation.test.mjs +169 -0
  191. package/tests/native-voyages-rpc.test.mjs +397 -0
  192. package/tests/native-voyages-ui.test.mjs +307 -0
  193. package/tests/opencode-atomic-write.test.mjs +55 -0
  194. package/tests/opencode-cli.test.mjs +1312 -0
  195. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  196. package/tests/opencode-delivery.test.mjs +16 -0
  197. package/tests/opencode-providers.test.mjs +245 -18
  198. package/tests/opencode-runtime.test.mjs +597 -8
  199. package/tests/opencode-session-controls.test.mjs +15 -0
  200. package/tests/opencode-ship-rpc.test.mjs +71 -0
  201. package/tests/opencode-update.test.mjs +475 -0
  202. package/tests/opencode-workspace.test.mjs +51 -2
  203. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  204. package/tests/release-workflow.test.mjs +46 -0
  205. package/tests/runtime-artifact.test.mjs +269 -0
  206. package/tests/runtime-development-trust.test.mjs +231 -0
  207. package/tests/runtime-github-transport.test.mjs +132 -0
  208. package/tests/runtime-token-env.test.mjs +23 -0
  209. package/tests/server.test.mjs +3 -1
  210. package/tests/ship-telemetry.test.mjs +1 -1
  211. package/tests/source-preview.test.mjs +253 -0
  212. package/tests/test_native_terminal_probe.py +76 -0
  213. package/tests/test_native_terminal_semantics.py +232 -0
  214. package/tests/update.test.mjs +42 -5
  215. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  216. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  217. package/tests/workspace-live.test.mjs +127 -0
  218. package/tools/benchmark/diagnose-request.mjs +59 -36
  219. package/tools/benchmark/eval-common.mjs +3 -3
  220. package/tools/benchmark/evaluators.test.mjs +4 -4
  221. package/tools/benchmark/guard.test.mjs +35 -2
  222. package/tools/benchmark/guard.ts +20 -7
  223. package/tools/benchmark/planning-startup.test.mjs +30 -34
  224. package/tools/benchmark/runner.mjs +12 -7
  225. package/tools/benchmark/runner.test.mjs +23 -7
  226. package/tools/benchmark/upstream-error.test.mjs +16 -2
  227. package/tools/generate-owned-runtime-release.mjs +217 -0
  228. package/tools/installed-runtime-contract.mjs +16 -0
  229. package/tools/manual-runtime-manifest.mjs +39 -0
  230. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  231. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  232. package/tools/native-fixture.mjs +199 -40
  233. package/tools/native-glyph-helpers.py +127 -0
  234. package/tools/native-glyph-pty.py +144 -0
  235. package/tools/native-manual-projects.mjs +259 -0
  236. package/tools/native-manual.mjs +338 -0
  237. package/tools/native-model-picker-acceptance.py +306 -0
  238. package/tools/native-owned-runtime-env.sh +47 -0
  239. package/tools/native-paired-guards.mjs +282 -0
  240. package/tools/native-paired-manifest.mjs +72 -0
  241. package/tools/native-paired-orchestration.sh +51 -0
  242. package/tools/native-plugin-readiness.mjs +33 -0
  243. package/tools/native-plugin-readiness.test.mjs +53 -0
  244. package/tools/native-pty-invocation.mjs +26 -0
  245. package/tools/native-pty-runtime.mjs +83 -0
  246. package/tools/native-pty-smoke.py +147 -24
  247. package/tools/native-public-repo-shell-result.mjs +13 -0
  248. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  249. package/tools/native-runtime-identity.mjs +201 -0
  250. package/tools/native-slash-acceptance-env.py +38 -0
  251. package/tools/native-terminal-semantics.py +876 -0
  252. package/tools/owned-shell-observer/index.ts +16 -0
  253. package/tools/owned-shell-observer/package.json +1 -0
  254. package/tools/owned-visual-clear-env.py +48 -0
  255. package/tools/owned-visual-clear-model.py +21 -0
  256. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  257. package/tools/prepare-native-manual.mjs +122 -0
  258. package/tools/run-native-terminal-paired.sh +403 -0
  259. package/tools/runtime-token-env.mjs +8 -0
  260. package/tools/seed-native-switch-fixture.mjs +79 -0
  261. package/tools/test-native-controls.mjs +3 -1
  262. package/tools/test-native-daily-quests.mjs +65 -0
  263. package/tools/test-native-default-cli.mjs +106 -0
  264. package/tools/test-native-delivery-host.mjs +102 -20
  265. package/tools/test-native-evaluation.mjs +7 -3
  266. package/tools/test-native-glyph-pty.mjs +94 -0
  267. package/tools/test-native-host.mjs +87 -63
  268. package/tools/test-native-outcomes.mjs +3 -1
  269. package/tools/test-native-package.mjs +206 -38
  270. package/tools/test-native-performance.mjs +26 -0
  271. package/tools/test-native-product.mjs +90 -0
  272. package/tools/test-native-pty.mjs +34 -9
  273. package/tools/test-native-public-repo.mjs +224 -0
  274. package/tools/test-native-refresh-reentry.mjs +106 -0
  275. package/tools/test-native-report.mjs +27 -20
  276. package/tools/test-native-review.mjs +75 -0
  277. package/tools/test-native-runtime.mjs +120 -0
  278. package/tools/test-native-ship-models.mjs +176 -0
  279. package/tools/test-native-ship-walkthrough.mjs +96 -0
  280. package/tools/test-native-slash-commands.mjs +125 -0
  281. package/tools/test-native-slash-commands.py +242 -0
  282. package/tools/test-native-swe-host.mjs +224 -55
  283. package/tools/test-native-tasks.mjs +77 -0
  284. package/tools/test-native-terminal-performance.py +887 -0
  285. package/tools/test-native-terminal-readiness.sh +85 -0
  286. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  287. package/tools/test-native-visual-clear-installed.mjs +48 -0
  288. package/tools/test-owned-host-controls.mjs +238 -0
  289. package/tools/test-owned-opencode-runtime.mjs +35 -0
  290. package/tools/test-owned-visual-clear-env.py +120 -0
  291. package/tools/test-owned-visual-clear.py +373 -0
  292. package/tools/verify-draft-runtime-gate.mjs +138 -0
  293. package/tools/verify-owned-runtime-release.mjs +114 -0
  294. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,252 @@
1
+ /**
2
+ * OpenCode-facing command-line adapter.
3
+ *
4
+ * The CLI deliberately depends on a small runtime port rather than importing
5
+ * OpenCode implementation modules. The native service owns process startup,
6
+ * authentication, model discovery, and session execution; this module owns
7
+ * stable ctg argv compatibility and output/exit-code behavior.
8
+ */
9
+
10
+ const VALUE_FLAGS = new Map([
11
+ ['--provider', 'provider'], ['--model', 'model'], ['--thinking', 'thinking'],
12
+ ['--theme', 'theme'], ['--engine-cli', 'engineCli'], ['--agent-cli', 'engineCli'],
13
+ ['--session', 'session'], ['--validators', 'validators'], ['--context', 'context'],
14
+ ['--bash-cap', 'bashCap'], ['--max-tools', 'maxTools'], ['--max-seconds', 'maxSeconds'],
15
+ ['--max-repairs', 'maxRepairs'], ['--review', 'review'], ['--glyphs', 'glyphs'],
16
+ ['--codegraph-max-files', 'codegraphMaxFiles'], ['--workflow', 'workflow'],
17
+ ['--rewrite-cap', 'rewriteCap'], ['--tool-output-cap', 'toolOutputCap'], ['--turn-delay-ms', 'turnDelayMs'],
18
+ ['--jev-provider', 'jevProvider'],
19
+ ]);
20
+
21
+ const BOOL_FLAGS = new Map([
22
+ ['--isolate', ['isolate', true]], ['--strict', ['strict', true]], ['--no-strict', ['strict', false]],
23
+ ['--no-delivery', ['delivery', false]], ['--delivery', ['delivery', true]],
24
+ ['--no-guide', ['autoFraming', false]], ['--guide', ['autoFraming', true]],
25
+ ['--ascii', ['ascii', true]], ['--json', ['json', true]], ['-p', ['print', true]],
26
+ ['--protect-existing', ['protectExisting', true]], ['--jev', ['jev', true]], ['--no-jev', ['jev', false]],
27
+ ['--print', ['print', true]], ['--demo', ['demo', true]], ['-c', ['continue', true]],
28
+ ['--continue', ['continue', true]], ['-r', ['resume', true]], ['--resume', ['resume', true]],
29
+ ]);
30
+
31
+ const aliases = new Map([
32
+ ['ui', 'tui'], ['interactive', 'tui'], ['sessions', 'resume'], ['s', 'status'],
33
+ ['fingerprint', 'hash'], ['c', 'check'], ['auth', 'login'], ['cloud', 'account'], ['upgrade', 'update'],
34
+ ['archives', 'archive'],
35
+ ['web', 'serve'], ['start', 'serve'],
36
+ ]);
37
+
38
+ export function parseOpenCodeArgs(argv) {
39
+ const options = {};
40
+ const prompt = [];
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const arg = argv[i];
43
+ if (arg === '--') {
44
+ prompt.push(...argv.slice(i + 1));
45
+ break;
46
+ }
47
+ if (VALUE_FLAGS.has(arg)) {
48
+ const value = argv[++i];
49
+ if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
50
+ options[VALUE_FLAGS.get(arg)] = value;
51
+ continue;
52
+ }
53
+ if (BOOL_FLAGS.has(arg)) {
54
+ const [key, value] = BOOL_FLAGS.get(arg);
55
+ options[key] = value;
56
+ continue;
57
+ }
58
+ if (arg.startsWith('-')) throw new Error(`Unknown flag: ${arg}`);
59
+ prompt.push(arg);
60
+ }
61
+ if (prompt.length) options.prompt = prompt.join(' ');
62
+ return options;
63
+ }
64
+
65
+ export function cliHelp(version = '') {
66
+ return `\n\x1b[1m\x1b[36mCarthagent\x1b[0m${version ? ` \x1b[90m${version}\x1b[0m` : ''} \x1b[2m(ctg)\x1b[0m
67
+
68
+ \x1b[1mUSAGE:\x1b[0m
69
+ ctg Launch the native Carthagent delivery console
70
+ ctg <task> Start a session and deliver <task>
71
+ ctg --list-models [search] List available provider models
72
+ ctg <command> [options]
73
+ ctg service stop Stop this profile’s background service safely
74
+ ctg review workspace --request-file <task.txt> [--provider <name> --model <id>]
75
+
76
+ \x1b[1mCOMMANDS:\x1b[0m
77
+ native [args] Compatibility alias; native is now the default
78
+ \x1b[32mtui\x1b[0m, \x1b[32mui\x1b[0m Interactive console
79
+ \x1b[32mresume\x1b[0m, \x1b[32msessions\x1b[0m Open the session picker
80
+ \x1b[32mcontinue\x1b[0m Continue the most recent session
81
+ \x1b[32mdemo\x1b[0m Run the scripted demonstration
82
+ \x1b[32mstatus\x1b[0m, \x1b[32ms\x1b[0m Display workspace fingerprint, contract and checks
83
+ \x1b[32mcheck\x1b[0m [id], \x1b[32mc\x1b[0m [id] Execute a declared check
84
+ \x1b[32mhash\x1b[0m, \x1b[32mfingerprint\x1b[0m Calculate workspace SHA-256 fingerprint
85
+ \x1b[32mvalidate\x1b[0m <file.json> Validate acceptance contract
86
+ \x1b[32mlogin\x1b[0m, \x1b[32mauth\x1b[0m Connect an AI provider
87
+ \x1b[32maccount\x1b[0m Show Ship credit, plan, grants and CLI sessions
88
+ \x1b[32mbilling\x1b[0m [checkout|portal] Open Ship billing
89
+ \x1b[32mlogout-cloud\x1b[0m [session] Revoke the current or named Ship session
90
+ \x1b[32marchive\x1b[0m [list|search|read|export] Read-only legacy session archive
91
+ \x1b[32mreview\x1b[0m Configure self-review or review a PR
92
+ \x1b[32mserve\x1b[0m, \x1b[32mweb\x1b[0m, \x1b[32mstart\x1b[0m Start the web dashboard
93
+ \x1b[32mtest\x1b[0m Run core tests
94
+ \x1b[32meval\x1b[0m [args...] Run evaluation harness
95
+ \x1b[32mupdate\x1b[0m [--check] [--channel stable|beta|experimental] Update Carthagent
96
+ \x1b[32mhelp\x1b[0m, \x1b[32m--help\x1b[0m, \x1b[32m-h\x1b[0m Display this help
97
+
98
+ \x1b[1mOPTIONS:\x1b[0m
99
+ --provider <name> Provider for the agent
100
+ --model <id> Model id or pattern
101
+ --thinking <level> off|minimal|low|medium|high|xhigh|max
102
+ --theme <name> Carthagent theme name
103
+ --engine-cli, --agent-cli <path> Explicit native runtime executable override
104
+ --session <path|id> Resume a specific session
105
+ --continue, -c Resume the most recent session
106
+ --resume, -r Open the session picker
107
+ --validators <file> Required-validator manifest
108
+ --context <file> Extra task context file
109
+ --bash-cap <sec> Cap shell tool timeouts
110
+ --max-tools <n> Session tool-call budget (0 disables)
111
+ --max-seconds <n> Session elapsed-time budget (0 disables)
112
+ --max-repairs <n> Automatic-repair budget (0 disables)
113
+ --protect-existing Preserve files present at session start
114
+ --rewrite-cap <n> Whole-file writes per path (0 disables)
115
+ --tool-output-cap <n> Bound text output per tool result
116
+ --turn-delay-ms <n> Pace tool loops
117
+ --jev, --no-jev Enable/disable advisory plan analysis (on for Ship models)
118
+ --jev-provider <name> ship|openrouter|typesafe
119
+ --review <mode> Self-review: ask|yes|no
120
+ --isolate Isolate extension/skill discovery (not a worktree)
121
+ --no-strict Disable delivery-plan write gate
122
+ --no-delivery Run without delivery behavior
123
+ --no-guide Disable prompt framing
124
+ -p, --print Headless text output
125
+ --json Headless JSONL events
126
+ `;
127
+ }
128
+
129
+ const commandNames = new Set([
130
+ 'tui', 'resume', 'continue', 'demo', 'status', 'hash', 'check', 'validate', 'login',
131
+ 'account', 'billing', 'logout-cloud', 'archive', 'serve', 'test', 'eval', 'review', 'update', 'quit', 'q', 'exit',
132
+ 'logout', 'service',
133
+ ]);
134
+
135
+ function fail(message, code = 1, io = console) {
136
+ io.error(message);
137
+ return code;
138
+ }
139
+
140
+ function ensureMethod(runtime, method) {
141
+ if (typeof runtime?.[method] !== 'function') {
142
+ throw new Error(`Native Carthagent CLI runtime does not provide ${method}()`);
143
+ }
144
+ return runtime[method].bind(runtime);
145
+ }
146
+
147
+ /**
148
+ * Execute a CLI invocation using the native runtime port.
149
+ *
150
+ * Runtime contract (all operations are real operations, not optional no-ops):
151
+ * run({mode, options, prompt, args}), listModels({query}), login(), account(),
152
+ * billing({action}), logout({session}), review({args}), domain({command,args}),
153
+ * and help/version metadata are supplied by the caller. Interactive vs
154
+ * headless selection is made here so all aliases share identical behavior.
155
+ */
156
+ export async function runOpenCodeCli({ argv = [], runtime, io = console, stdoutIsTTY = Boolean(process.stdout.isTTY), version = '' } = {}) {
157
+ const first = argv[0];
158
+ const normalized = aliases.get(first) || first;
159
+ const rest = commandNames.has(normalized) ? argv.slice(1) : (first === '--list-models' ? [] : argv);
160
+
161
+ if (['help', '--help', '-h'].includes(first)) {
162
+ io.log(cliHelp(version));
163
+ return 0;
164
+ }
165
+ if (['--version', '-v'].includes(first)) {
166
+ io.log(version);
167
+ return 0;
168
+ }
169
+ if (first === '--list-models') {
170
+ try { await ensureMethod(runtime, 'listModels')({ query: argv.slice(1).join(' ') }); return 0; }
171
+ catch (error) {
172
+ if (error?.name === 'AbortError' || error?.code === 'SIGINT') return 130;
173
+ return fail(error.message, 1, io);
174
+ }
175
+ }
176
+ if (['status', 'hash', 'fingerprint', 'check', 'c', 'validate'].includes(normalized)) {
177
+ try { return Number((await ensureMethod(runtime, 'domain')({ command: normalized, args: rest })) ?? 0); }
178
+ catch (error) { return fail(error.message, 1, io); }
179
+ }
180
+ if (normalized === 'review') {
181
+ try { return Number((await ensureMethod(runtime, 'review')({ args: rest, cwd: process.cwd() })) ?? 0); }
182
+ catch (error) { return fail(error.message, error.exitCode ?? 1, io); }
183
+ }
184
+ if (normalized === 'login' || normalized === 'auth') {
185
+ if (!stdoutIsTTY) return fail('login requires an interactive terminal — or set a provider API key environment variable', 1, io);
186
+ try { return Number((await ensureMethod(runtime, 'login')({ args: rest })) ?? 0); }
187
+ catch (error) {
188
+ if (error?.name === 'AbortError' || error?.code === 'SIGINT') return 130;
189
+ return fail(error.message, 1, io);
190
+ }
191
+ }
192
+ if (normalized === 'logout') return fail('Use `logout-cloud` to revoke a Carthagent Ship session; provider credentials are managed by the native runtime.', 1, io);
193
+ if (normalized === 'account' || normalized === 'cloud') {
194
+ try { return Number((await ensureMethod(runtime, 'account')({ args: rest })) ?? 0); }
195
+ catch (error) { return fail(error.message, 1, io); }
196
+ }
197
+ if (normalized === 'billing') {
198
+ const action = rest[0] || 'checkout';
199
+ if (!['checkout', 'portal'].includes(action)) return fail('Usage: ctg billing [checkout|portal]', 1, io);
200
+ try { return Number((await ensureMethod(runtime, 'billing')({ action })) ?? 0); }
201
+ catch (error) { return fail(error.message, 1, io); }
202
+ }
203
+ if (normalized === 'logout-cloud') {
204
+ try { return Number((await ensureMethod(runtime, 'logout')({ session: rest[0] })) ?? 0); }
205
+ catch (error) { return fail(error.message, 1, io); }
206
+ }
207
+ if (normalized === 'archive') {
208
+ try { return Number((await ensureMethod(runtime, 'archive')({ args: rest })) ?? 0); }
209
+ catch (error) { return fail(error.message, error.exitCode ?? 1, io); }
210
+ }
211
+ if (['serve', 'service', 'test', 'eval', 'update', 'quit', 'q', 'exit', 'demo'].includes(normalized)) {
212
+ try { return Number((await ensureMethod(runtime, 'command')({ command: normalized, args: rest })) ?? 0); }
213
+ catch (error) { return fail(error.message, error.exitCode ?? 1, io); }
214
+ }
215
+
216
+ let mode = normalized;
217
+ let args = rest;
218
+ if (first === undefined) { mode = 'tui'; args = []; }
219
+ else if (!commandNames.has(normalized)) {
220
+ if (first.startsWith('-') && !VALUE_FLAGS.has(first) && !BOOL_FLAGS.has(first)) {
221
+ io.error(`Unknown command or flag: ${first}`);
222
+ io.log(cliHelp(version));
223
+ return 1;
224
+ }
225
+ mode = 'tui';
226
+ }
227
+ if (mode === 'resume') mode = 'tui';
228
+ if (mode === 'continue') mode = 'tui';
229
+ if (mode === 'demo') mode = 'tui';
230
+
231
+ let options;
232
+ try { options = parseOpenCodeArgs(args); }
233
+ catch (error) { return fail(error.message, 1, io); }
234
+ if (normalized === 'resume' || normalized === 'continue' || normalized === 'demo') {
235
+ options[normalized === 'resume' ? 'resume' : normalized === 'continue' ? 'continue' : 'demo'] = true;
236
+ }
237
+ if (options.demo && !stdoutIsTTY) return fail('demo requires an interactive terminal', 1, io);
238
+ const headless = options.print || options.json || !stdoutIsTTY;
239
+ if (headless && options.resume) { options.resume = false; options.continue = true; }
240
+ if (headless && !options.prompt) return fail('non-interactive mode requires a prompt', 1, io);
241
+ try { return Number((await ensureMethod(runtime, 'run')({ mode: headless ? 'headless' : 'interactive', options, prompt: options.prompt, args })) ?? 0); }
242
+ catch (error) {
243
+ if (error?.name === 'AbortError' || error?.code === 'SIGINT') return 130;
244
+ return fail(error.message, Number.isInteger(error?.exitCode) ? error.exitCode : 1, io);
245
+ }
246
+ }
247
+
248
+ export const openCodeCliManifest = Object.freeze({
249
+ aliases: Object.freeze(Object.fromEntries(aliases)),
250
+ valueFlags: Object.freeze(Object.fromEntries(VALUE_FLAGS)),
251
+ boolFlags: Object.freeze(Object.fromEntries([...BOOL_FLAGS].map(([flag, [key, value]]) => [flag, { key, value }]))),
252
+ });
@@ -8,6 +8,8 @@ import os from 'node:os';
8
8
  import { shipAgentDir } from '../ship-auth.mjs';
9
9
  import { CARTHAGENT_SHIP_PROVIDER_ID } from '../providers/names.mjs';
10
10
  import { withFileLock } from './file-lock.mjs';
11
+ import { Credential } from '@opencode/schema/credential';
12
+ import { Schema } from 'effect';
11
13
 
12
14
  export const CTG_CREDENTIAL_NAMESPACE = 'ctg.credentials/';
13
15
  const refreshFlights = new Map();
@@ -45,12 +47,60 @@ export function nativeCredentialFile(env = process.env, platform = process.platf
45
47
  return join(dataHome, 'opencode', 'auth.json');
46
48
  }
47
49
 
50
+ /** OpenCode 2.0.32 packages/cli/src/database-path.ts and util/global-roots.ts.
51
+ * Unlike legacy auth.json, the native credential vault is in the channel DB.
52
+ * The running host supplies its channel; never infer it from a release label. */
53
+ export function nativeCredentialDatabaseFile(env = process.env, channel, home = os.homedir()) {
54
+ if (!channel) throw new Error('The native runtime channel is required to locate its credential vault.');
55
+ const data = join(env.XDG_DATA_HOME || join(home, '.local', 'share'), 'opencode');
56
+ const filename = env.OPENCODE_DB ?? (['latest', 'dev', 'beta', 'next', 'prod'].includes(channel)
57
+ || ['1', 'true'].includes(env.OPENCODE_DISABLE_CHANNEL_DB)
58
+ ? 'opencode.db' : `opencode-${channel.replace(/[^a-zA-Z0-9._-]/g, '-')}.db`);
59
+ if (filename === ':memory:') throw new Error('Native OAuth refresh requires a file-backed credential vault.');
60
+ return resolve(data, filename);
61
+ }
62
+
63
+ /** Raw read only: connection.resolve() refreshes expired OAuth and must never
64
+ * be called from its own refresh callback. Schema provenance: pinned 2.0.32
65
+ * packages/core/src/credential/sql.ts; value is Credential.Value JSON. Opening
66
+ * read-only must fail for missing/changed vaults, never create a parallel store.
67
+ * Bun is the native host; node:sqlite supports the same regression under Node.
68
+ * @param {{credentialFile: string, credentialID?: string, integrationID: string, methodID: string, refreshToken?: string}} options
69
+ */
70
+ export async function readNativeOAuthCredential({ credentialFile, credentialID = undefined, integrationID, methodID, refreshToken = undefined }) {
71
+ const db = typeof globalThis.Bun !== 'undefined'
72
+ ? new (await import('bun:sqlite')).Database(credentialFile, { readonly: true, strict: true })
73
+ : new (await import('node:sqlite')).DatabaseSync(credentialFile, { readOnly: true });
74
+ try {
75
+ // Capture the callback's original row, never the current active selection.
76
+ // LIMIT 2 detects duplicate grants without selecting an arbitrary account.
77
+ const query = credentialID
78
+ ? 'SELECT id, value FROM credential WHERE id = ? AND integration_id = ?'
79
+ : `SELECT id, value FROM credential WHERE integration_id = ? AND json_extract(value, '$.type') = 'oauth'
80
+ AND json_extract(value, '$.methodID') = ? AND json_extract(value, '$.refresh') = ? LIMIT 2`;
81
+ const statement = typeof db.prepare === 'function' ? db.prepare(query) : db.query(query);
82
+ const rows = credentialID ? statement.all(credentialID, integrationID) : statement.all(integrationID, methodID, refreshToken);
83
+ if (rows.length > 1) throw new Error('Ambiguous native OAuth credential identity; no refresh was attempted.');
84
+ const row = rows[0];
85
+ if (!row) throw new Error('The native OAuth credential is no longer stored; reconnect before refreshing.');
86
+ // Do not include SQLite/JSON/schema errors or their input values in logs.
87
+ let value;
88
+ try { value = Schema.decodeUnknownSync(Credential.Value)(JSON.parse(row.value)); }
89
+ catch { throw new Error('Unsupported native credential vault value; no refresh was attempted.'); }
90
+ if (value.type !== 'oauth' || value.methodID !== methodID) {
91
+ throw new Error('The native OAuth credential method changed; no refresh was attempted.');
92
+ }
93
+ return credentialID ? value : row.id;
94
+ } finally { db.close(); }
95
+ }
96
+
48
97
  /** Serialize refresh across host processes, rereading OpenCode's vault under the lock.
49
- * @param {{credential: any, credentialFile: string, reread: () => Promise<any>, refresh: (credential: any) => Promise<any>, timeout?: number}} options
98
+ * @param {{credential: any, credentialFile: string, credentialID?: string, reread: () => Promise<any>, refresh: (credential: any) => Promise<any>, timeout?: number}} options
50
99
  */
51
100
  export async function refreshNativeCredential({
52
101
  credential,
53
102
  credentialFile,
103
+ credentialID,
54
104
  reread,
55
105
  refresh,
56
106
  timeout = 15_000,
@@ -63,6 +113,8 @@ export async function refreshNativeCredential({
63
113
  // Token material is never written to the lease path or logs.
64
114
  const digest = createHash('sha256').update(`${credential.methodID || ''}\0${credential.refresh}`).digest('hex').slice(0, 32);
65
115
  const lock = `${profile}.ctg-refresh-${digest}.lock`;
116
+ // Keep the existing token-scoped lock path so old hosts cannot rotate in
117
+ // parallel. Old unbound receipts are uncertain and must not be adopted.
66
118
  const pending = `${lock}.pending`;
67
119
  const credentialDigest = value => createHash('sha256')
68
120
  .update(`${value?.methodID || credential.methodID || ''}\0${value?.refresh || ''}`).digest('hex').slice(0, 32);
@@ -74,7 +126,6 @@ export async function refreshNativeCredential({
74
126
  if (persistedReceipt === receipt?.raw) await rm(pending, { force: true });
75
127
  return latest;
76
128
  };
77
- const current = await reread();
78
129
  // OpenCode persists refresh callback results only after this callback
79
130
  // resolves. Leave a token-free handoff receipt so another host that wins
80
131
  // the disk lock during that gap waits for the native vault write instead
@@ -82,9 +133,13 @@ export async function refreshNativeCredential({
82
133
  let receipt;
83
134
  try {
84
135
  receipt = JSON.parse(await readFile(pending, 'utf8'));
85
- receipt.raw = JSON.stringify({ pid: receipt.pid, owner: receipt.owner, from: receipt.from, to: receipt.to, stage: receipt.stage, createdAt: receipt.createdAt });
136
+ receipt.raw = JSON.stringify({ pid: receipt.pid, owner: receipt.owner, from: receipt.from, to: receipt.to, stage: receipt.stage, createdAt: receipt.createdAt, credentialID: receipt.credentialID });
86
137
  }
87
138
  catch (error) { if (error.code !== 'ENOENT') throw new Error('Invalid pending native credential refresh receipt; inspect the CTG refresh receipt before retrying.'); }
139
+ if (credentialID && receipt && receipt.credentialID !== credentialID) {
140
+ throw new Error('Unbound or different native credential refresh receipt; no rotation was attempted. Reauthenticate or inspect the receipt before retrying.');
141
+ }
142
+ const current = await reread();
88
143
  if (current?.type === 'oauth' && current.refresh && current.refresh !== credential.refresh) {
89
144
  if (receipt?.from === digest && receipt.to === credentialDigest(current)) {
90
145
  const persistedReceipt = await readFile(pending, 'utf8').catch(() => '');
@@ -108,22 +163,22 @@ export async function refreshNativeCredential({
108
163
  // authoritative read before treating the outcome as uncertain.
109
164
  const persisted = await acceptPersistedRotation();
110
165
  if (persisted) return persisted;
111
- throw new Error('The native refresh owner exited before OpenCode saved its credential. No second rotation was attempted; inspect the CTG refresh receipt.');
166
+ throw new Error('The native refresh owner exited before Carthagent runtime saved its credential. No second rotation was attempted; inspect the CTG refresh receipt.');
112
167
  }
113
168
  await new Promise(resolve => setTimeout(resolve, 40));
114
169
  }
115
- throw new Error('Timed out waiting for OpenCode to persist its refreshed credential; no second refresh was attempted.');
170
+ throw new Error('Timed out waiting for Carthagent runtime to persist its refreshed credential; no second refresh was attempted.');
116
171
  }
117
172
 
118
173
  const owner = randomUUID();
119
- const initialReceipt = JSON.stringify({ pid: process.pid, owner, from: digest, stage: 'refreshing', createdAt: Date.now() });
174
+ const initialReceipt = JSON.stringify({ pid: process.pid, owner, from: digest, stage: 'refreshing', createdAt: Date.now(), credentialID });
120
175
  const pendingHandle = await open(pending, 'wx', 0o600);
121
176
  try { await pendingHandle.writeFile(initialReceipt); await pendingHandle.sync(); }
122
177
  finally { await pendingHandle.close(); }
123
178
  const saveReceipt = async value => {
124
179
  const temporary = `${pending}.${owner}.${value.stage}.tmp`;
125
180
  const handle = await open(temporary, 'wx', 0o600);
126
- try { await handle.writeFile(JSON.stringify(value)); await handle.sync(); }
181
+ try { await handle.writeFile(JSON.stringify({ ...value, credentialID })); await handle.sync(); }
127
182
  finally { await handle.close(); }
128
183
  if (await readFile(pending, 'utf8').catch(() => '') !== initialReceipt) {
129
184
  await rm(temporary, { force: true });
@@ -201,5 +256,5 @@ export async function legacyOAuthForOpenCode(options = {}) {
201
256
  }
202
257
 
203
258
  export function legacyCredentialMigrationGuidance() {
204
- return 'Existing Carthagent Ship OAuth can be migrated from the legacy auth.json credential into OpenCode integration storage; other provider credentials remain BYOK and may be reconnected through OpenCode if their format is unsupported.';
259
+ return 'Existing Carthagent Ship OAuth can be migrated from the legacy auth.json credential into Carthagent runtime integration storage; other provider credentials remain BYOK and may be reconnected through Carthagent runtime if their format is unsupported.';
205
260
  }
@@ -0,0 +1,53 @@
1
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { runCommand } from '../delivery.mjs';
5
+
6
+ /** Run an honest, isolated offline walkthrough; it never contacts a provider. */
7
+ /** @param {{ io?: Console, execute?: typeof runCommand, signal?: AbortSignal, nodeBinary?: string }} options */
8
+ export async function runOfflineDemo({ io = console, execute = runCommand, signal, nodeBinary = process.env.CTG_NODE_EXECUTABLE || process.execPath } = {}) {
9
+ signal?.throwIfAborted();
10
+ const workspace = await mkdtemp(join(tmpdir(), 'carthagent-native-demo-'));
11
+ try {
12
+ await mkdir(join(workspace, 'test'));
13
+ await writeFile(join(workspace, 'app.mjs'), `export function summarize(items) {
14
+ return items.filter(item => item.done).map(item => item.title).join(', ');
15
+ }
16
+ `);
17
+ await writeFile(join(workspace, 'test', 'app.test.mjs'), `import test from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import { summarize } from '../app.mjs';
20
+
21
+ test('summarize includes completed items and preserves their order', () => {
22
+ assert.equal(summarize([{ title: 'Plan', done: true }, { title: 'Ship', done: false }, { title: 'Test', done: true }]), 'Plan, Test');
23
+ });
24
+
25
+ test('summarize handles an empty list', () => {
26
+ assert.equal(summarize([]), '');
27
+ });
28
+ `);
29
+ io.log('Carthagent native offline demo');
30
+ io.log('Scripted fixture only: no provider/API calls, no edits to your project, and no claim of project delivery evidence.');
31
+ io.log('\nPlan: implement a tiny completed-task summarizer, then verify ordering and the empty case.');
32
+ io.log(`\nFixture workspace: ${workspace}`);
33
+ const env = { ...process.env };
34
+ // Node's test runner marks subprocesses as nested workers; clear the marker
35
+ // so the fixture really runs when /demo is itself exercised from node:test.
36
+ delete env.NODE_TEST_CONTEXT;
37
+ const result = await execute([nodeBinary, '--test', 'test/app.test.mjs'], { cwd: workspace, timeoutSeconds: 30, signal, env });
38
+ if (result.output?.trim()) io.log(`\n${result.output.trim()}`);
39
+ if (result.cancelled || signal?.aborted) {
40
+ io.log('\nOffline demo cancelled; no completion is claimed.');
41
+ return 130;
42
+ }
43
+ if (result.code !== 0 || result.timedOut || result.cancelled) {
44
+ io.error(`Offline demo check failed${result.timedOut ? ' (timed out)' : ''}: ${result.output?.trim() || 'no test output'}`);
45
+ return 1;
46
+ }
47
+ io.log('\nVerification: isolated Node test suite passed (2 tests).');
48
+ io.log('Finish: the sample behavior is verified only inside this disposable demo fixture.');
49
+ return 0;
50
+ } finally {
51
+ await rm(workspace, { recursive: true, force: true });
52
+ }
53
+ }
@@ -84,6 +84,9 @@ export default Plugin.define({
84
84
  await pendingWrites.get(event.sessionID);
85
85
  const session = await ctx.session.get({ sessionID: event.sessionID });
86
86
  const configured = session.metadata?.[budgetKey];
87
+ // This hook is an admission guard for evaluation sessions, not a
88
+ // general model-request gate for unrelated native host workflows.
89
+ if (configured === undefined) return;
87
90
  if (!configured || typeof configured !== 'object'
88
91
  || configured.maxTurns !== maxTurns || configured.maxCost !== maxCost) {
89
92
  throw Error('Evaluation session budget handshake is missing or differs from the active admission guard');
@@ -37,13 +37,14 @@ async function bounded(promise, milliseconds, fallback) {
37
37
 
38
38
  /** Run one native OpenCode session with fail-closed accounting and cancellation. */
39
39
  export async function runNativeEvaluation({ directory, prompt, systemPrompt = '', model, provider, timeoutSeconds = 600, maxTurns = 100, maxCost = 3,
40
- signal, onEvent = () => {}, client, baseUrl = process.env.CTG_OPENCODE_URL, headers = {}, version = '2.0.32', outcomeTimeoutSeconds = 10 }) {
40
+ signal, onEvent = () => {}, client, baseUrl = process.env.CTG_OPENCODE_URL, headers = {}, version = '2.0.32', outcomeTimeoutSeconds = 10,
41
+ agent, metadata = {} }) {
41
42
  positive(timeoutSeconds, 'timeoutSeconds', 3600);
42
43
  positive(maxTurns, 'maxTurns', 1000);
43
44
  positive(maxCost, 'maxCost');
44
45
  positive(outcomeTimeoutSeconds, 'outcomeTimeoutSeconds', 60);
45
46
  if (!directory || !prompt || !model || !provider) throw Error('directory, prompt, provider, and model are required');
46
- if (!client && !baseUrl) throw Error('CTG_OPENCODE_URL must identify the isolated native OpenCode service; shared service discovery is disabled.');
47
+ if (!client && !baseUrl) throw Error('CTG_OPENCODE_URL must identify the isolated native Carthagent runtime service; shared service discovery is disabled.');
47
48
  if (!client && !Object.keys(headers).length && process.env.CTG_OPENCODE_AUTHORIZATION) headers = { authorization: process.env.CTG_OPENCODE_AUTHORIZATION };
48
49
  const api = client || OpenCode.make({ baseUrl, headers });
49
50
  const controller = new AbortController();
@@ -55,7 +56,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
55
56
  const timer = setTimeout(() => controller.abort(new Error('Evaluation wall-time limit')), timeoutSeconds * 1000);
56
57
  const started = Date.now();
57
58
  const state = { turns: 0, productiveTurns: 0, providerErrors: 0, cost: null, tokens: 0, tools: {}, errors: [], lastStop: null, budgetReason: null, deliveryStatus: null };
58
- let session, streamTask, streamError, outcome;
59
+ let session, streamTask, streamError, outcome, actualRuntimeVersion;
59
60
  let interruptionStatus = 'not-needed';
60
61
  let finished = false;
61
62
  let stopAfterIdle = false;
@@ -76,7 +77,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
76
77
  state.lastStop = data.finish;
77
78
  } else if (event.type === 'session.execution.failed') {
78
79
  state.providerErrors++;
79
- state.errors.push(data.error?.message || 'OpenCode execution failed');
80
+ state.errors.push(data.error?.message || 'Carthagent runtime execution failed');
80
81
  } else if (event.type === 'session.tool.called') {
81
82
  toolByCallID.set(data.id, data);
82
83
  const name = data.tool || data.name;
@@ -106,7 +107,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
106
107
  do {
107
108
  const info=await api.session.get({sessionID:session.id},{signal:AbortSignal.timeout(5000)});
108
109
  if(info.outcome) return info;
109
- if(Date.now()>=deadline) throw Error('OpenCode session terminal outcome did not become available');
110
+ if(Date.now()>=deadline) throw Error('Carthagent runtime session terminal outcome did not become available');
110
111
  await new Promise(resolve=>setTimeout(resolve,25));
111
112
  } while(true);
112
113
  };
@@ -114,15 +115,20 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
114
115
  try {
115
116
  if (controller.signal.aborted) throw controller.signal.reason;
116
117
  const info = await api.server.info({ signal: controller.signal });
117
- if (info.version && !info.version.startsWith('2.')) throw Error(`OpenCode 2 required, received ${info.version}`);
118
+ actualRuntimeVersion = info.version;
119
+ const ownedRuntimeVersion = `0.0.0-ctg.${version}`;
120
+ if (actualRuntimeVersion !== version && actualRuntimeVersion !== ownedRuntimeVersion) {
121
+ throw Error(`Carthagent runtime compatible with ${version} required, received ${actualRuntimeVersion ?? 'unknown'}`);
122
+ }
118
123
  const plugins = await api.plugin.list({ location: { directory } }, { signal: controller.signal });
119
124
  const guard = plugins.data.find(plugin => plugin.id === EVALUATION_BUDGET_PLUGIN);
120
125
  if (guard?.state.status !== 'active') throw Error(`Evaluation admission guard ${EVALUATION_BUDGET_PLUGIN} is not active`);
121
126
  session = await api.session.create({
127
+ ...(agent ? { agent } : {}),
122
128
  model: { providerID: provider, id: model },
123
129
  location: { directory },
124
130
  title: `CTG evaluation ${NATIVE_EVALUATION_RUNTIME}`,
125
- metadata: { [EVALUATION_BUDGET_METADATA]: { maxTurns, maxCost } },
131
+ metadata: { ...metadata, [EVALUATION_BUDGET_METADATA]: { maxTurns, maxCost } },
126
132
  }, { signal: controller.signal });
127
133
  streamTask = onEventStream();
128
134
  // Subscription has no replay; allow its connection to establish before admission.
@@ -140,7 +146,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
140
146
  // authoritative terminal outcome is visible; absence is never success.
141
147
  const sessionInfo = await pollOutcome();
142
148
  outcome = sessionInfo.outcome;
143
- if (outcome !== 'succeeded') state.errors.push(`OpenCode session ended with authoritative outcome: ${outcome}`);
149
+ if (outcome !== 'succeeded') state.errors.push(`Carthagent runtime session ended with authoritative outcome: ${outcome}`);
144
150
 
145
151
  // Fetch only this session's assistant transcript; /session/stats is project-wide.
146
152
  const transcript = await allMessages(api, session.id, { signal: AbortSignal.timeout(15000) });
@@ -159,7 +165,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
159
165
  }
160
166
  const tokenUsage = sessionInfo.tokens;
161
167
  if (!Number.isFinite(sessionInfo.cost) || sessionInfo.cost < 0 || !tokenUsage || !['input','output','reasoning'].every(key => Number.isFinite(tokenUsage[key]) && tokenUsage[key] >= 0) || !tokenUsage.cache || !['read','write'].every(key => Number.isFinite(tokenUsage.cache[key]) && tokenUsage.cache[key] >= 0)) {
162
- throw Error('OpenCode session aggregate usage is unavailable; refusing evaluation without enforceable max-cost');
168
+ throw Error('Carthagent runtime session aggregate usage is unavailable; refusing evaluation without enforceable max-cost');
163
169
  }
164
170
  state.cost = sessionInfo.cost;
165
171
  state.tokens = tokenUsage.input + tokenUsage.output + tokenUsage.reasoning + tokenUsage.cache.read + tokenUsage.cache.write;
@@ -177,7 +183,7 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
177
183
  if (controller.signal.aborted && Date.now() - started >= timeoutSeconds * 1000) state.budgetReason = 'wall-time limit';
178
184
  else if (controller.signal.aborted && signal?.aborted && /wall.?time/i.test(String(signal.reason?.message || signal.reason || ''))) state.budgetReason = 'wall-time limit';
179
185
  else if (controller.signal.aborted && signal?.aborted) state.budgetReason = 'cancelled';
180
- else state.budgetReason = 'native OpenCode request failed';
186
+ else state.budgetReason = 'native Carthagent runtime request failed';
181
187
  }
182
188
  state.errors.push(error instanceof Error ? error.message : String(error));
183
189
  if (session && !outcome) {
@@ -236,7 +242,21 @@ export async function runNativeEvaluation({ directory, prompt, systemPrompt = ''
236
242
  }
237
243
  if (!outcome && !state.budgetReason) {
238
244
  state.budgetReason = 'missing terminal session outcome';
239
- state.errors.push('OpenCode session has no authoritative terminal outcome.');
245
+ state.errors.push('Carthagent runtime session has no authoritative terminal outcome.');
240
246
  }
241
- return { ...state, outcome: outcome || null, interruptionStatus, exitCode: state.budgetReason || streamError ? 1 : 0, timedOut: state.budgetReason === 'wall-time limit', durationMs: Date.now() - started, sessionID: session?.id || null, runtime: NATIVE_EVALUATION_RUNTIME, opencodeVersion: version };
247
+ return {
248
+ ...state,
249
+ outcome: outcome || null,
250
+ interruptionStatus,
251
+ exitCode: state.budgetReason || streamError ? 1 : 0,
252
+ timedOut: state.budgetReason === 'wall-time limit',
253
+ durationMs: Date.now() - started,
254
+ sessionID: session?.id || null,
255
+ runtime: NATIVE_EVALUATION_RUNTIME,
256
+ compatibilityVersion: version,
257
+ runtimeVersion: actualRuntimeVersion ?? null,
258
+ // Kept for consumers of the prior result shape; this is the actual server
259
+ // version, not the compatibility target supplied to the evaluator.
260
+ opencodeVersion: actualRuntimeVersion ?? null,
261
+ };
242
262
  }
@@ -26,7 +26,7 @@ export async function runHeadless(service, workspace, prompt, options = {}) {
26
26
  created = await createSession(service, workspace, { body: options.session ?? {}, signal });
27
27
  id = created?.id ?? created?.sessionID;
28
28
  }
29
- if (!id) throw new Error('OpenCode did not return a valid session identifier');
29
+ if (!id) throw new Error('Carthagent runtime did not return a valid session identifier');
30
30
  session = created ?? { id };
31
31
  await setSessionEnvironment(service, id, options.env ?? process.env, { location: workspace, signal });
32
32
  promptStarted = true;
@@ -35,7 +35,7 @@ export async function runHeadless(service, workspace, prompt, options = {}) {
35
35
  signal,
36
36
  body: { ...(options.prompt ?? {}), ...(options.parts ? { parts: options.parts } : {}), text: prompt },
37
37
  });
38
- await serviceRequest(service, 'POST', `/api/experimental/session/${encodeURIComponent(id)}/wait`, { signal, location: workspace });
38
+ await waitForSession(service, id, workspace, signal);
39
39
  session = await waitForOutcome(service, id, workspace, { deadline, signal, accepted });
40
40
  const fetched = await sessionMessages(service, id, { signal, location: workspace });
41
41
  const messages = currentTurnMessages(chronologicalMessages(fetched), accepted, options.maxMessages);
@@ -72,6 +72,19 @@ export async function runHeadless(service, workspace, prompt, options = {}) {
72
72
  }
73
73
  }
74
74
 
75
+ async function waitForSession(service, id, workspace, signal) {
76
+ // The host keeps executing when Node's HTTP headers deadline expires on this
77
+ // long-held observation request. Reattach the observer, never replay prompt.
78
+ for (;;) {
79
+ signal.throwIfAborted();
80
+ try {
81
+ return await serviceRequest(service, 'POST', `/api/experimental/session/${encodeURIComponent(id)}/wait`, { signal, location: workspace });
82
+ } catch (error) {
83
+ if (signal.aborted || (error?.cause?.code ?? error?.code) !== 'UND_ERR_HEADERS_TIMEOUT') throw error;
84
+ }
85
+ }
86
+ }
87
+
75
88
  function preAdmissionResult(sessionID, reason, cause, abortReason) {
76
89
  const outcome = reason === 'aborted' ? 'interrupted' : 'timed-out';
77
90
  return {
@@ -123,18 +136,22 @@ async function incompleteResult(service, sessionID, workspace, accepted, session
123
136
  const fetched = await sessionMessages(service, sessionID, { location: workspace, signal: AbortSignal.timeout(5000) }).catch(() => []);
124
137
  const messages = currentTurnMessages(chronologicalMessages(fetched), accepted);
125
138
  const idle = terminalIdle(messages);
126
- const outcome = session.outcome ?? idle?.outcome ?? (reason === 'aborted' ? 'interrupted' : undefined);
139
+ // The session-level outcome may belong to a previously completed turn. Only
140
+ // an idle message bounded to this accepted prompt can establish a terminal
141
+ // outcome for the current turn.
142
+ const outcome = idle?.outcome;
127
143
  const result = extractAssistant(messages, accepted);
128
144
  const reasonText = session.ctg?.reason ?? reason;
129
145
  const finalOutcome = outcome ?? (reason === 'timed-out' ? 'timed-out' : 'interrupted');
146
+ const { outcome: _priorOutcome, ...sessionWithoutPriorOutcome } = session;
130
147
  return {
131
- session: { ...session, ...(outcome ? { outcome } : {}), error: session.error ?? idle?.error },
148
+ session: { ...sessionWithoutPriorOutcome, ...(outcome ? { outcome } : {}), error: session.error ?? idle?.error },
132
149
  accepted,
133
150
  messages,
134
151
  result,
135
152
  outcome: finalOutcome,
136
153
  error: finalOutcome === 'failed' ? result?.error ?? session.error ?? idle?.error : undefined,
137
- terminal: Boolean(session.outcome || idle),
154
+ terminal: Boolean(idle),
138
155
  reason: reasonText,
139
156
  budgetReason: finalOutcome === 'timed-out' || finalOutcome === 'failed' ? reasonText : undefined,
140
157
  cause: cause?.message,