carthagent 1.0.7 → 1.1.0-beta.2

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 (303) 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/cloud-onboarding-acceptance.md +65 -0
  6. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  7. package/docs/glyph-review/test-isolation-v1.md +26 -0
  8. package/docs/glyph-review/typecheck-base.log +1 -0
  9. package/docs/glyph-review/typecheck-v4.log +1 -0
  10. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  11. package/docs/help-dispatch-review.md +69 -0
  12. package/docs/launch-readiness-live-results.md +65 -0
  13. package/docs/legacy-cli.md +150 -0
  14. package/docs/live-theme-plugin-api.md +18 -0
  15. package/docs/native-cli.md +102 -0
  16. package/docs/native-compatibility.md +16 -0
  17. package/docs/native-daily-quests.md +132 -0
  18. package/docs/native-glyph-pty.md +59 -0
  19. package/docs/native-manual-testing.md +336 -0
  20. package/docs/native-slash-acceptance-handoff.md +49 -0
  21. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  22. package/docs/native-terminal-performance.md +310 -0
  23. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  24. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  25. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  34. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  35. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  36. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  37. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  38. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  39. package/docs/native-terminal-semantics.md +89 -0
  40. package/docs/native-upgrade.md +175 -0
  41. package/docs/opencode-composed-source-review.md +89 -0
  42. package/docs/opencode-migration.md +225 -9
  43. package/docs/opencode-owned-runtime.md +241 -0
  44. package/docs/opencode-runtime.md +12 -0
  45. package/docs/opencode-theme-provider-proof/README.md +55 -0
  46. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  47. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  48. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  49. package/docs/owned-host-controls-proof.md +39 -0
  50. package/docs/owned-runtime-release-manifest.md +42 -0
  51. package/docs/owned-visual-clear-proof.md +40 -0
  52. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  53. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  54. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  55. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  61. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  62. package/docs/release-notes-beta.1.md +60 -0
  63. package/docs/release-notes-beta.2.md +60 -0
  64. package/docs/release-notes-native-draft.md +73 -0
  65. package/docs/release-notes-preview.10.md +121 -0
  66. package/docs/release-notes-preview.11.md +76 -0
  67. package/docs/release-notes-preview.12.md +55 -0
  68. package/docs/release-notes-preview.13.md +68 -0
  69. package/docs/release-notes-preview.9.md +130 -0
  70. package/docs/source-preview.md +120 -0
  71. package/evaluate.mjs +28 -1
  72. package/lib/budget.mjs +1 -1
  73. package/lib/cloud/client.mjs +13 -7
  74. package/lib/codegraph.mjs +1 -1
  75. package/lib/delivery-review.mjs +19 -6
  76. package/lib/delivery.mjs +3 -2
  77. package/lib/environment.mjs +8 -0
  78. package/lib/guidance.mjs +1 -1
  79. package/lib/opencode/cli-runtime.mjs +1188 -0
  80. package/lib/opencode/cli.mjs +253 -0
  81. package/lib/opencode/credentials.mjs +63 -8
  82. package/lib/opencode/demo.mjs +53 -0
  83. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  84. package/lib/opencode/evaluation.mjs +32 -12
  85. package/lib/opencode/headless.mjs +22 -5
  86. package/lib/opencode/provider-policy.mjs +23 -0
  87. package/lib/opencode/review.mjs +37 -16
  88. package/lib/opencode/runtime-artifact.mjs +335 -0
  89. package/lib/opencode/runtime-github-transport.mjs +121 -0
  90. package/lib/opencode/runtime-release.json +36 -0
  91. package/lib/opencode/runtime.mjs +299 -20
  92. package/lib/opencode/service.mjs +77 -35
  93. package/lib/opencode/session-options.mjs +6 -2
  94. package/lib/opencode/update.mjs +222 -0
  95. package/lib/opencode/workspace.mjs +1 -1
  96. package/lib/planning-advice.mjs +2 -2
  97. package/lib/providers/experiential.mjs +2 -2
  98. package/lib/providers/names.mjs +1 -1
  99. package/lib/tui/framing.mjs +1 -80
  100. package/lib/tui/models.mjs +1 -1
  101. package/lib/update.mjs +10 -1
  102. package/package.json +33 -7
  103. package/plugins/carthagent/README.md +23 -0
  104. package/plugins/carthagent/src/bootstrap.ts +1 -1
  105. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  106. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  107. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  108. package/plugins/carthagent/src/index.ts +71 -24
  109. package/plugins/carthagent/src/rpc.ts +51 -6
  110. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  111. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  112. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  113. package/plugins/carthagent/src/server/delivery.ts +227 -112
  114. package/plugins/carthagent/src/server/framing.mjs +80 -0
  115. package/plugins/carthagent/src/server/policy.ts +1 -1
  116. package/plugins/carthagent/src/server/providers.ts +65 -36
  117. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  118. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  119. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  120. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  121. package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
  122. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  123. package/plugins/carthagent/src/testing.ts +5 -0
  124. package/plugins/carthagent/src/tui.tsx +506 -0
  125. package/plugins/carthagent/src/ui/client.ts +127 -0
  126. package/plugins/carthagent/src/ui/commands.ts +70 -0
  127. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  128. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  129. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  130. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  131. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  132. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  133. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  134. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  135. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  136. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  137. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  138. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  139. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  140. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  141. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  142. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  143. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  144. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  145. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  146. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  147. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  148. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  149. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  150. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  151. package/plugins/carthagent/tests/commands.test.ts +107 -0
  152. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  153. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  154. package/plugins/carthagent/themes/carthage.json +67 -0
  155. package/plugins/carthagent/tui.ts +26 -1
  156. package/script/ctg/build-owned-runtime.sh +166 -0
  157. package/tests/budget-engine.test.mjs +4 -3
  158. package/tests/cli.test.mjs +52 -96
  159. package/tests/cloud-client.test.mjs +2 -2
  160. package/tests/daily-evidence.test.mjs +62 -0
  161. package/tests/delivery-review.test.mjs +57 -2
  162. package/tests/delivery.test.mjs +22 -1
  163. package/tests/experiential-extension.test.mjs +2 -2
  164. package/tests/experiential.test.mjs +1 -1
  165. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  166. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  167. package/tests/installed-runtime-contract.test.mjs +43 -0
  168. package/tests/native-cli-host-smoke.mjs +457 -0
  169. package/tests/native-daily-editor-focus.test.mjs +213 -0
  170. package/tests/native-daily-editor.test.mjs +43 -0
  171. package/tests/native-daily-quests-layout.test.mjs +55 -0
  172. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  173. package/tests/native-daily-quests-ui.test.mjs +778 -0
  174. package/tests/native-daily-source.test.mjs +525 -0
  175. package/tests/native-demo-ui.test.mjs +48 -0
  176. package/tests/native-evaluation-admission.test.mjs +24 -0
  177. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  178. package/tests/native-fixture-runtime.test.mjs +111 -0
  179. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  180. package/tests/native-glyph-pty.test.mjs +107 -0
  181. package/tests/native-manual-projects.test.mjs +170 -0
  182. package/tests/native-model-command.test.mjs +67 -0
  183. package/tests/native-owned-runtime-env.test.mjs +140 -0
  184. package/tests/native-paired-guards.test.mjs +464 -0
  185. package/tests/native-provider-policy.test.mjs +33 -0
  186. package/tests/native-pty-invocation.test.mjs +114 -0
  187. package/tests/native-pty-runtime.test.mjs +45 -0
  188. package/tests/native-resume-session.test.mjs +46 -0
  189. package/tests/native-review-consumer.test.mjs +286 -0
  190. package/tests/native-runtime-identity.test.mjs +214 -0
  191. package/tests/native-select-mouse.test.mjs +23 -0
  192. package/tests/native-ship-guidance.test.mjs +16 -0
  193. package/tests/native-ship-parity.test.mjs +526 -0
  194. package/tests/native-ship-plan.test.mjs +85 -0
  195. package/tests/native-ship-rpc.test.mjs +83 -0
  196. package/tests/native-ui-commands.test.mjs +6 -0
  197. package/tests/native-voyages-layout.test.mjs +28 -0
  198. package/tests/native-voyages-navigation.test.mjs +169 -0
  199. package/tests/native-voyages-rpc.test.mjs +397 -0
  200. package/tests/native-voyages-ui.test.mjs +307 -0
  201. package/tests/opencode-atomic-write.test.mjs +55 -0
  202. package/tests/opencode-cli.test.mjs +1312 -0
  203. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  204. package/tests/opencode-delivery.test.mjs +16 -0
  205. package/tests/opencode-providers.test.mjs +248 -20
  206. package/tests/opencode-runtime.test.mjs +597 -8
  207. package/tests/opencode-session-controls.test.mjs +15 -0
  208. package/tests/opencode-ship-rpc.test.mjs +71 -0
  209. package/tests/opencode-update.test.mjs +475 -0
  210. package/tests/opencode-workspace.test.mjs +51 -2
  211. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  212. package/tests/release-workflow.test.mjs +46 -0
  213. package/tests/runtime-artifact.test.mjs +269 -0
  214. package/tests/runtime-development-trust.test.mjs +231 -0
  215. package/tests/runtime-github-transport.test.mjs +132 -0
  216. package/tests/runtime-token-env.test.mjs +23 -0
  217. package/tests/server.test.mjs +3 -1
  218. package/tests/ship-telemetry.test.mjs +1 -1
  219. package/tests/source-preview.test.mjs +253 -0
  220. package/tests/test_native_terminal_probe.py +76 -0
  221. package/tests/test_native_terminal_semantics.py +232 -0
  222. package/tests/tui.test.mjs +2 -2
  223. package/tests/update.test.mjs +42 -5
  224. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  225. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  226. package/tests/workspace-live.test.mjs +127 -0
  227. package/tools/benchmark/diagnose-request.mjs +59 -36
  228. package/tools/benchmark/eval-common.mjs +3 -3
  229. package/tools/benchmark/evaluators.test.mjs +4 -4
  230. package/tools/benchmark/guard.test.mjs +35 -2
  231. package/tools/benchmark/guard.ts +20 -7
  232. package/tools/benchmark/planning-startup.test.mjs +30 -34
  233. package/tools/benchmark/runner.mjs +12 -7
  234. package/tools/benchmark/runner.test.mjs +23 -7
  235. package/tools/benchmark/upstream-error.test.mjs +16 -2
  236. package/tools/generate-owned-runtime-release.mjs +217 -0
  237. package/tools/installed-runtime-contract.mjs +16 -0
  238. package/tools/manual-runtime-manifest.mjs +39 -0
  239. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  240. package/tools/native-fixture-stop-diagnostics.mjs +45 -0
  241. package/tools/native-fixture.mjs +199 -40
  242. package/tools/native-glyph-helpers.py +127 -0
  243. package/tools/native-glyph-pty.py +144 -0
  244. package/tools/native-manual-projects.mjs +259 -0
  245. package/tools/native-manual.mjs +338 -0
  246. package/tools/native-model-picker-acceptance.py +306 -0
  247. package/tools/native-owned-runtime-env.sh +47 -0
  248. package/tools/native-paired-guards.mjs +282 -0
  249. package/tools/native-paired-manifest.mjs +72 -0
  250. package/tools/native-paired-orchestration.sh +51 -0
  251. package/tools/native-plugin-readiness.mjs +33 -0
  252. package/tools/native-plugin-readiness.test.mjs +53 -0
  253. package/tools/native-pty-invocation.mjs +26 -0
  254. package/tools/native-pty-runtime.mjs +83 -0
  255. package/tools/native-pty-smoke.py +147 -24
  256. package/tools/native-public-repo-shell-result.mjs +13 -0
  257. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  258. package/tools/native-runtime-identity.mjs +201 -0
  259. package/tools/native-slash-acceptance-env.py +38 -0
  260. package/tools/native-terminal-semantics.py +882 -0
  261. package/tools/owned-shell-observer/index.ts +16 -0
  262. package/tools/owned-shell-observer/package.json +1 -0
  263. package/tools/owned-visual-clear-env.py +48 -0
  264. package/tools/owned-visual-clear-model.py +21 -0
  265. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  266. package/tools/prepare-native-manual.mjs +122 -0
  267. package/tools/run-native-terminal-paired.sh +403 -0
  268. package/tools/runtime-token-env.mjs +8 -0
  269. package/tools/seed-native-switch-fixture.mjs +79 -0
  270. package/tools/test-native-controls.mjs +3 -1
  271. package/tools/test-native-daily-quests.mjs +65 -0
  272. package/tools/test-native-default-cli.mjs +106 -0
  273. package/tools/test-native-delivery-host.mjs +102 -20
  274. package/tools/test-native-evaluation.mjs +7 -3
  275. package/tools/test-native-glyph-pty.mjs +94 -0
  276. package/tools/test-native-host.mjs +91 -65
  277. package/tools/test-native-outcomes.mjs +3 -1
  278. package/tools/test-native-package.mjs +206 -38
  279. package/tools/test-native-performance.mjs +26 -0
  280. package/tools/test-native-product.mjs +90 -0
  281. package/tools/test-native-pty.mjs +34 -9
  282. package/tools/test-native-public-repo.mjs +224 -0
  283. package/tools/test-native-refresh-reentry.mjs +106 -0
  284. package/tools/test-native-report.mjs +27 -20
  285. package/tools/test-native-review.mjs +75 -0
  286. package/tools/test-native-runtime.mjs +120 -0
  287. package/tools/test-native-ship-models.mjs +176 -0
  288. package/tools/test-native-ship-walkthrough.mjs +96 -0
  289. package/tools/test-native-slash-commands.mjs +125 -0
  290. package/tools/test-native-slash-commands.py +242 -0
  291. package/tools/test-native-swe-host.mjs +224 -55
  292. package/tools/test-native-tasks.mjs +77 -0
  293. package/tools/test-native-terminal-performance.py +887 -0
  294. package/tools/test-native-terminal-readiness.sh +85 -0
  295. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  296. package/tools/test-native-visual-clear-installed.mjs +48 -0
  297. package/tools/test-owned-host-controls.mjs +238 -0
  298. package/tools/test-owned-opencode-runtime.mjs +35 -0
  299. package/tools/test-owned-visual-clear-env.py +120 -0
  300. package/tools/test-owned-visual-clear.py +373 -0
  301. package/tools/verify-draft-runtime-gate.mjs +138 -0
  302. package/tools/verify-owned-runtime-release.mjs +114 -0
  303. package/tools/verify-release-version.mjs +21 -1
@@ -2,7 +2,7 @@
2
2
  /** Offline real-host test. Uses a disposable HOME and never discovers the user's service. */
3
3
  import assert from 'node:assert/strict';
4
4
  import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
5
- import { join, resolve } from 'node:path';
5
+ import { dirname, join, resolve } from 'node:path';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { execFile, spawn } from 'node:child_process';
8
8
  import { createServer } from 'node:http';
@@ -10,14 +10,29 @@ import { once } from 'node:events';
10
10
  import { randomBytes } from 'node:crypto';
11
11
  import { promisify } from 'node:util';
12
12
  import { OpenCode } from '@opencode/client';
13
- import { resolveOpenCode } from '../lib/opencode/runtime.mjs';
13
+ import { resolveOpenCode, runtimeIdentity as getRuntimeIdentity } from '../lib/opencode/runtime.mjs';
14
+ import { loadNativeTestRuntime, preflightNativeTestRuntimeArtifact } from './native-runtime-identity.mjs';
14
15
  import { cancelSession } from '../lib/opencode/service.mjs';
15
16
  import { runHeadless } from '../lib/opencode/headless.mjs';
16
17
 
17
18
  const root = resolve(import.meta.dirname, '..');
18
19
  const plugin = resolve(process.argv[2] ?? join(root, 'plugins/carthagent'));
19
- const archiveFixture = resolve(root, 'tests/fixtures/archive-plugin');
20
- const binary = await resolveOpenCode();
20
+ const runtimeOptions = {};
21
+ if (process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST) {
22
+ runtimeOptions.runtimeReleaseManifestPath = process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST;
23
+ }
24
+ if (process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT) {
25
+ await preflightNativeTestRuntimeArtifact({
26
+ ownedRoot: process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT,
27
+ expectedManifestPath: process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST,
28
+ });
29
+ runtimeOptions.acquireOwnedRuntime = async () => process.env.CTG_TEST_RUNTIME_ARTIFACT_ROOT;
30
+ }
31
+ const binary = await resolveOpenCode(runtimeOptions);
32
+ const productionIdentity = await getRuntimeIdentity(binary, runtimeOptions);
33
+ const ownedRoot = productionIdentity.kind === 'ctg-owned' ? dirname(dirname(dirname(binary))) : undefined;
34
+ const expectedManifest = ownedRoot ? JSON.parse(await readFile(process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST ?? join(root, 'lib/opencode/runtime-release.json'), 'utf8')) : undefined;
35
+ const runtimeIdentity = await loadNativeTestRuntime({ binary, ownedRoot, expectedManifest });
21
36
  const execFileAsync = promisify(execFile);
22
37
  const temp = await mkdtemp(join(tmpdir(), 'ctg-host-smoke-'));
23
38
  const password = randomBytes(24).toString('hex');
@@ -25,35 +40,6 @@ const providerKey = `sentinel-${randomBytes(24).toString('hex')}`;
25
40
  let child;
26
41
  let childClosed = false;
27
42
  let output = '';
28
- let deviceAuthorizationBody;
29
- const gatewayAuth = [];
30
- const gatewayServer = createServer((request, response) => {
31
- gatewayAuth.push({ authorization: request.headers.authorization, url: request.url });
32
- response.writeHead(200, { 'content-type': 'application/json' });
33
- response.end(JSON.stringify(request.url?.endsWith('/models')
34
- ? { data: [{ id: 'sentinel-model' }] }
35
- : { id: 'chatcmpl-sentinel', object: 'chat.completion', choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }));
36
- });
37
- await new Promise((resolveListen, reject) => gatewayServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
38
- const gatewayAddress = gatewayServer.address();
39
- const authServer = createServer((request, response) => {
40
- let body = '';
41
- request.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
42
- request.on('end', () => {
43
- if (request.url === '/v1/device/authorizations') {
44
- deviceAuthorizationBody = JSON.parse(body);
45
- response.writeHead(201, { 'content-type': 'application/json' });
46
- response.end(JSON.stringify({ deviceCode: 'offline-device-code', userCode: 'OFFLINE', verificationUri: 'http://127.0.0.1/verify', expiresIn: 120, interval: 1 }));
47
- } else if (request.url === '/v1/device/token') {
48
- response.writeHead(400, { 'content-type': 'application/json' });
49
- response.end(JSON.stringify({ error: 'authorization_pending' }));
50
- } else {
51
- response.writeHead(404).end();
52
- }
53
- });
54
- });
55
- await new Promise((resolveListen, reject) => authServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
56
- const authAddress = authServer.address();
57
43
  let fixtureRequests = 0;
58
44
  const fixture = createServer(async (req, res) => {
59
45
  fixtureRequests += 1;
@@ -93,27 +79,66 @@ const fixture = createServer(async (req, res) => {
93
79
  res.end('data: [DONE]\n\n');
94
80
  });
95
81
  await new Promise((resolveListen, reject) => fixture.once('error', reject).listen(0, '127.0.0.1', resolveListen));
82
+ let deviceAuthorizationBody;
83
+ const gatewayAuth = [];
84
+ const gatewayServer = createServer((request, response) => {
85
+ gatewayAuth.push({ authorization: request.headers.authorization, url: request.url });
86
+ response.writeHead(200, { 'content-type': 'application/json' });
87
+ response.end(JSON.stringify(request.url?.endsWith('/models')
88
+ ? { data: [{ id: 'sentinel-model' }] }
89
+ : { id: 'chatcmpl-sentinel', object: 'chat.completion', choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }));
90
+ });
91
+ await new Promise((resolveListen, reject) => gatewayServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
92
+ const gatewayAddress = gatewayServer.address();
93
+ const authServer = createServer((request, response) => {
94
+ let body = '';
95
+ request.setEncoding('utf8').on('data', (chunk) => { body += chunk; });
96
+ request.on('end', () => {
97
+ if (request.url === '/v1/device/authorizations') {
98
+ deviceAuthorizationBody = JSON.parse(body);
99
+ response.writeHead(201, { 'content-type': 'application/json' });
100
+ response.end(JSON.stringify({ deviceCode: 'offline-device-code', userCode: 'OFFLINE', verificationUri: 'http://127.0.0.1/verify', expiresIn: 120, interval: 1 }));
101
+ } else if (request.url === '/v1/device/token') {
102
+ response.writeHead(400, { 'content-type': 'application/json' });
103
+ response.end(JSON.stringify({ error: 'authorization_pending' }));
104
+ } else {
105
+ response.writeHead(404).end();
106
+ }
107
+ });
108
+ });
109
+ await new Promise((resolveListen, reject) => authServer.listen(0, '127.0.0.1', resolveListen).once('error', reject));
110
+ const authAddress = authServer.address();
96
111
  try {
97
112
  const cwd = join(temp, 'project');
98
113
  const config = join(temp, 'config/opencode');
99
- const homeConfig = join(temp, 'home/.config/opencode');
100
114
  const legacy = join(temp, 'home/.carthagent/agent/sessions');
115
+ const homeConfig = join(temp, 'home/.config/opencode');
116
+ const ancestorConfig = join(temp, '.opencode');
101
117
  await Promise.all([
102
- mkdir(cwd), mkdir(config, { recursive: true }), mkdir(homeConfig, { recursive: true }), mkdir(legacy, { recursive: true }),
103
- mkdir(join(temp, 'appdata'), { recursive: true }), mkdir(join(temp, 'localappdata'), { recursive: true }),
118
+ mkdir(cwd),
119
+ mkdir(config, { recursive: true }),
120
+ mkdir(legacy, { recursive: true }),
121
+ mkdir(join(temp, 'appdata'), { recursive: true }),
122
+ mkdir(join(temp, 'localappdata'), { recursive: true }),
123
+ mkdir(homeConfig, { recursive: true }),
124
+ mkdir(ancestorConfig, { recursive: true }),
104
125
  ]);
126
+ await writeFile(join(legacy, 'legacy.jsonl'), '{"type":"session","id":"legacy-fixture"}\n{"type":"message","message":{"role":"user","content":"fixture needle"}}\nmalformed\n');
105
127
  await writeFile(join(homeConfig, 'opencode.json'), JSON.stringify({ model: 'home-poison/model', plugins: ['home-poison-plugin'] }));
106
128
  await writeFile(join(cwd, 'opencode.json'), JSON.stringify({ model: 'project-poison/model', plugins: ['project-poison-plugin'] }));
107
- await writeFile(join(legacy, 'legacy.jsonl'), '{"type":"session","id":"legacy-fixture"}\n{"type":"message","message":{"role":"user","content":"fixture needle"}}\nmalformed\n');
129
+ await writeFile(join(temp, 'opencode.json'), JSON.stringify({ model: 'ancestor-poison/model', plugins: ['ancestor-poison-plugin'] }));
130
+ await writeFile(join(ancestorConfig, 'opencode.json'), JSON.stringify({ model: 'hidden-ancestor-poison/model', plugins: ['hidden-ancestor-poison-plugin'] }));
108
131
  await writeFile(join(config, 'opencode.json'), JSON.stringify({
109
- plugins: [plugin, archiveFixture], update: 'disable',
132
+ plugins: [plugin], update: 'disable',
110
133
  model: 'ctgfixture/fixture',
111
134
  providers: {
112
135
  ctgfixture: {
113
136
  name: 'CTG local prompt fixture',
114
137
  package: '@opencode/ai/providers/openai-compatible',
115
138
  settings: { baseURL: `http://127.0.0.1:${fixture.address().port}/v1` },
116
- models: { fixture: { name: 'Fixture', limit: { context: 8192, output: 1024 } } },
139
+ // This fixture tests host attachment, not compaction; leave room for the
140
+ // production tool catalog and the preceding multi-turn wrapper checks.
141
+ models: { fixture: { name: 'Fixture', limit: { context: 32768, output: 1024 } } },
117
142
  },
118
143
  },
119
144
  }));
@@ -160,12 +185,12 @@ try {
160
185
  });
161
186
  const request = { signal: AbortSignal.timeout(90_000) };
162
187
  const info = await client.server.info(request);
163
- assert.equal(info.version, '2.0.32');
188
+ assert.equal(info.version, runtimeIdentity.runtimeVersion);
164
189
  const isolationFetch = async (url, init) => {
165
190
  const response = await fetch(url, init);
166
191
  if (new URL(url).pathname === '/api/config') {
167
192
  const entries = await response.clone().json();
168
- assert.ok(!entries.some((entry) => entry.type === 'document' && [join(homeConfig, 'opencode.json'), join(cwd, 'opencode.json')].includes(entry.path)), `home and workspace config must not be discovered: ${JSON.stringify(entries)}`);
193
+ assert.ok(!entries.some((entry) => entry.type === 'document' && [homeConfig, cwd, temp, ancestorConfig].map(directory => join(directory, 'opencode.json')).includes(entry.path)), `home, workspace and ancestor config must not be discovered: ${JSON.stringify(entries)}`);
169
194
  assert.ok(!JSON.stringify(entries).includes('poison'), `foreign config/plugin discovery must not leak into the CTG profile: ${JSON.stringify(entries)}`);
170
195
  }
171
196
  return response;
@@ -189,14 +214,14 @@ try {
189
214
  const deadline = Date.now() + 15_000;
190
215
  do {
191
216
  plugins = await client.plugin.list({ location: { directory: cwd } }, request);
192
- if (plugins.data.some((entry) => entry.id === 'carthagent')) break;
217
+ const current = plugins.data.find((entry) => entry.id === 'carthagent');
218
+ if (current?.state.status === 'active') break;
219
+ if (current?.state.status === 'error') throw new Error(`Carthagent plugin setup failed: ${JSON.stringify(current)}`);
193
220
  await new Promise((done) => setTimeout(done, 200));
194
221
  } while (Date.now() < deadline);
195
222
  const nativePlugin = plugins.data.find((entry) => entry.id === 'carthagent');
196
- const archivePlugin = plugins.data.find((entry) => entry.id === 'ctg-archive-fixture');
197
223
  assert.ok(nativePlugin, `Carthagent plugin must become visible: ${JSON.stringify(plugins.data)}`);
198
224
  assert.equal(nativePlugin.state.status, 'active');
199
- assert.equal(archivePlugin?.state.status, 'active', 'archive registration fixture must become visible');
200
225
  const providers = await client.provider.list({ location: { directory: cwd } }, request);
201
226
  assert.ok(providers.data.some((provider) => provider.id === 'experiential-labs'),
202
227
  'plugin setup must register the Ship provider with the real host');
@@ -205,8 +230,10 @@ try {
205
230
  assert.ok(ship, 'the native provider catalog must supply a real Ship integration record');
206
231
  assert.ok(ship.methods.some(({ type, id }) => type === 'oauth' && id === 'carthagent-device'),
207
232
  'plugin integration transform must register the Ship device-OAuth method');
208
- assert.ok(ship.methods.some(({ type }) => type === 'key'),
209
- 'plugin integration transform must register the Ship API-key method');
233
+ assert.equal(ship.methods.some(({ type }) => type === 'key'), false,
234
+ 'fresh Cloud sign-in must hide the advanced API-key method');
235
+ assert.equal(ship.methods.find(({ type, id }) => type === 'oauth' && id === 'carthagent-device').form?.length ?? 0, 0,
236
+ 'fresh Cloud sign-in must not prompt for legacy import');
210
237
  assert.ok(ship.methods.some(({ type, names }) => type === 'env' && names?.includes('EXPLABS_API_KEY')),
211
238
  'the native integration catalog resolves BYOK from the named environment credential');
212
239
  const models = await client.model.list({ location: { directory: cwd } }, request);
@@ -231,19 +258,10 @@ try {
231
258
  const health = await client.rpc.call({
232
259
  rpcID: 'carthagent', method: 'health', input: {}, location: { directory: cwd },
233
260
  }, request);
234
- console.log(JSON.stringify({ version: info.version, plugin: nativePlugin, health }, null, 2));
261
+ assert.equal(health.output.hostVersion, runtimeIdentity.runtimeVersion);
262
+ console.log(JSON.stringify({ version: info.version, compatibilityVersion: runtimeIdentity.compatibilityVersion, plugin: nativePlugin, health }, null, 2));
235
263
  assert.equal(health.output.directory, cwd);
236
264
  assert.equal(health.output.protocol, 1);
237
- const auth = await client.rpc.call({
238
- rpcID: 'carthagent', method: 'authStart', input: {}, location: { directory: cwd },
239
- }, request);
240
- assert.equal(auth.output.userCode, 'OFFLINE');
241
- assert.match(auth.output.flowId, /^[0-9a-f-]{36}$/);
242
- assert.match(deviceAuthorizationBody.installId, /^[0-9a-f-]{36}$/);
243
- assert.equal((await client.rpc.call({
244
- rpcID: 'carthagent', method: 'authCancel', input: { flowId: auth.output.flowId }, location: { directory: cwd },
245
- }, request)).output.cancelled, true);
246
- console.log('Device authorization start/cancel RPC passed against offline OpenCode host.');
247
265
  const echo = await client.rpc.call({
248
266
  rpcID: 'carthagent', method: 'echo', input: { text: 'native RPC fixture' }, location: { directory: cwd },
249
267
  }, request);
@@ -303,7 +321,7 @@ try {
303
321
  attached.once('close', (code, signal) => {
304
322
  clearTimeout(timer);
305
323
  if (code === 0) resolveRun(stdout);
306
- else reject(new Error(`Attached native CLI exited ${code ?? signal}: ${stderr}`));
324
+ else reject(new Error(`Attached native CLI exited ${code ?? signal}: ${stderr}\n${stdout}`));
307
325
  });
308
326
  });
309
327
  assert.match(attachedRun, /CTG_FIXTURE_OK/, 'the packaged CLI should attach to and prompt the ensured service');
@@ -327,6 +345,16 @@ try {
327
345
  } while (Date.now() < ptyDeadline);
328
346
  assert.equal(ptyState.data.status, 'running', 'native TUI should attach and remain active in the service-owned PTY');
329
347
  await client.pty.remove({ ptyID, location: { directory: cwd } }, request);
348
+ const auth = await client.rpc.call({
349
+ rpcID: 'carthagent', method: 'authStart', input: {}, location: { directory: cwd },
350
+ }, request);
351
+ assert.equal(auth.output.userCode, 'OFFLINE');
352
+ assert.match(auth.output.flowId, /^[0-9a-f-]{36}$/);
353
+ assert.match(deviceAuthorizationBody.installId, /^[0-9a-f-]{36}$/);
354
+ assert.equal((await client.rpc.call({
355
+ rpcID: 'carthagent', method: 'authCancel', input: { flowId: auth.output.flowId }, location: { directory: cwd },
356
+ }, request)).output.cancelled, true);
357
+ console.log('Device authorization start/cancel RPC passed against offline OpenCode host.');
330
358
  await assert.rejects(client.rpc.call({
331
359
  rpcID: 'carthagent', method: 'health', input: { unexpected: true }, location: { directory: cwd },
332
360
  }, request));
@@ -350,14 +378,14 @@ try {
350
378
  await assert.rejects(archiveCall('search', { query: ' ' }), (error) => error.type === 'archive_error');
351
379
  await assert.rejects(archiveCall('list', { limit: 101 }), (error) => error.type === 'rpc.invalid_input');
352
380
  assert.equal(await readFile(join(legacy, 'legacy.jsonl'), 'utf8'), '{"type":"session","id":"legacy-fixture"}\n{"type":"message","message":{"role":"user","content":"fixture needle"}}\nmalformed\n');
353
- console.log('Native plugin, archive RPC methods, output schemas, and declared failures passed against OpenCode 2.0.32.');
381
+ console.log(`Native plugin, archive RPC methods, output schemas, and declared failures passed against OpenCode ${runtimeIdentity.runtimeVersion} (compatibility ${runtimeIdentity.compatibilityVersion}).`);
354
382
  } catch (error) {
355
383
  console.error(JSON.stringify({ fixtureRequests }));
356
384
  console.error(output.replaceAll(password, '[redacted]'));
357
385
  throw error;
358
386
  } finally {
359
387
  if (child) {
360
- const closed = childClosed ? Promise.resolve() : once(child, 'close').then(() => { childClosed = true; });
388
+ const closed = childClosed ? Promise.resolve() : once(child, 'close');
361
389
  if (child.exitCode === null && child.signalCode === null) {
362
390
  if (process.platform === 'win32') {
363
391
  // This PID is the exact host process spawned above; /T is needed to
@@ -376,10 +404,8 @@ try {
376
404
  await closed;
377
405
  }
378
406
  }
379
- await Promise.all([
380
- new Promise((resolveClose) => authServer.close(resolveClose)),
381
- new Promise((resolveClose) => gatewayServer.close(resolveClose)),
382
- new Promise((resolveClose) => fixture.close(resolveClose)),
383
- ]);
407
+ await new Promise((resolveClose) => fixture.close(resolveClose));
408
+ await new Promise((resolveClose) => authServer.close(resolveClose));
409
+ await new Promise((resolveClose) => gatewayServer.close(resolveClose));
384
410
  await rm(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 250 });
385
411
  }
@@ -21,6 +21,7 @@ const host = await nativeFixture({
21
21
  } else streamText(res);
22
22
  },
23
23
  });
24
+ const identity = host.runtimeIdentity;
24
25
 
25
26
  async function bounded(promise, message) {
26
27
  let timer;
@@ -63,7 +64,8 @@ try {
63
64
  assert.ok(history.some(message => message.type === 'idle' && message.outcome === 'interrupted'));
64
65
  assert.equal(history.at(-1).outcome, 'succeeded');
65
66
  assert.deepEqual(host.providerErrors, []);
66
- console.log(JSON.stringify({ host: '2.0.32', failure: 'passed', cancellation: 'passed',
67
+ console.log(JSON.stringify({ host: identity.runtimeVersion, compatibilityVersion: identity.compatibilityVersion,
68
+ ...(identity.binarySha256 ? { binarySha256: identity.binarySha256 } : {}), failure: 'passed', cancellation: 'passed',
67
69
  concurrentIsolation: 'passed', recovery: 'passed', queuedInterruption: 'passed', cancellationMs }));
68
70
  } catch (error) {
69
71
  console.error(host.output);
@@ -1,70 +1,238 @@
1
1
  #!/usr/bin/env node
2
2
  /** Build and install the published artifact in isolation, then exercise its owned host. */
3
3
  import assert from 'node:assert/strict';
4
+ import { createRequire } from 'node:module';
4
5
  import { spawnSync } from 'node:child_process';
5
- import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
6
+ import { dirname, join as joinPath } from 'node:path';
7
+ import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
6
8
  import { tmpdir } from 'node:os';
7
9
  import { join, resolve } from 'node:path';
8
10
  import { fileURLToPath } from 'node:url';
11
+ import { withoutRuntimeGithubToken } from './runtime-token-env.mjs';
9
12
 
10
13
  const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
11
14
  const temp = await mkdtemp(join(tmpdir(), 'ctg-native-package-'));
15
+ const focused = process.env.CTG_NATIVE_PACKAGE_TEST;
16
+ const requireOwnedRuntime = process.argv.includes('--require-owned') || process.env.CTG_REQUIRE_OWNED_RUNTIME === '1';
17
+ assert.ok(!requireOwnedRuntime || !focused, 'owned-runtime release acceptance cannot be narrowed to a focused probe');
12
18
  const run = (command, args, options = {}) => {
13
- const result = spawnSync(command, args, { cwd: root, encoding: 'utf8', ...options });
19
+ const result = spawnSync(command, args, { cwd: root, encoding: 'utf8', timeout: 300_000, ...options });
20
+ if (result.error) throw new Error(`${command} ${args.join(' ')} failed: ${result.error.message}`, { cause: result.error });
14
21
  if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} failed:\n${result.stdout}\n${result.stderr}`);
15
22
  return result.stdout;
16
23
  };
17
24
 
25
+ // A package gate must exercise the normal installed-product acquisition path,
26
+ // not a developer's binary, cache, profile, or running service. Keep every
27
+ // OpenCode data location under this invocation's temporary directory.
28
+ const env = { ...process.env };
29
+ for (const key of Object.keys(env)) {
30
+ if (/^(?:CTG_OPENCODE_|CTG_OWNED_OPENCODE_RUNTIME$|CTG_RUNTIME_(?!GITHUB_TOKEN$)|CTG_TEST_|OPENCODE_|CARTHAGENT_(?:AGENT_DIR|CODING_AGENT_DIR|CONFIG|CLI)|PI_CODING_AGENT_DIR$)/i.test(key)) delete env[key];
31
+ }
32
+ const isolatedHome = join(temp, 'runtime-home');
33
+ Object.assign(env, {
34
+ HOME: isolatedHome,
35
+ USERPROFILE: isolatedHome,
36
+ APPDATA: join(temp, 'appdata'),
37
+ LOCALAPPDATA: join(temp, 'localappdata'),
38
+ XDG_CONFIG_HOME: join(temp, 'config'),
39
+ XDG_DATA_HOME: join(temp, 'data'),
40
+ XDG_STATE_HOME: join(temp, 'state'),
41
+ XDG_CACHE_HOME: join(temp, 'cache'),
42
+ CTG_OPENCODE_HOME: join(temp, 'ctg-runtime'),
43
+ CARTHAGENT_AGENT_DIR: join(temp, 'agent'),
44
+ CARTHAGENT_CODING_AGENT_DIR: join(temp, 'agent'),
45
+ PI_CODING_AGENT_DIR: join(temp, 'agent'),
46
+ CARTHAGENT_CONFIG: join(temp, 'carthagent-config.json'),
47
+ npm_config_cache: join(temp, 'npm-cache'),
48
+ });
49
+
18
50
  try {
19
- const packed = JSON.parse(run('npm', ['pack', '--json', '--pack-destination', temp]));
51
+ await mkdir(isolatedHome, { recursive: true });
52
+ const npmEnv = withoutRuntimeGithubToken(env);
53
+ const packed = JSON.parse(run('npm', ['pack', '--json', '--pack-destination', temp], { env: npmEnv }));
20
54
  const tarball = join(temp, packed[0].filename);
21
55
  const paths = packed[0].files.map((file) => file.path);
22
56
  assert.ok(paths.includes('plugins/carthagent/index.ts'), 'pack must include plugin server entrypoint');
23
57
  assert.ok(paths.includes('plugins/carthagent/tui.ts'), 'pack must include plugin TUI entrypoint');
58
+ assert.ok(paths.includes('tests/native-cli-host-smoke.mjs'), 'pack must include the production-backed native CLI smoke');
59
+ assert.ok(paths.includes('tools/test-native-refresh-reentry.mjs'), 'pack must include the expired native OAuth refresh regression');
60
+ assert.ok(paths.includes('tests/fixtures/native-discovery-plugin/server.ts'), 'pack must include the exact project-discovery fixture required by the native CLI smoke');
61
+ assert.ok(paths.includes('tests/fixtures/archive-plugin/index.ts'), 'pack must retain the separately allowlisted archive fixture');
62
+ assert.ok(!paths.includes('tests/fixtures/plan-advisor-corpus.json'), 'pack must not broaden fixture inclusion beyond explicitly required package fixtures');
24
63
  assert.ok(!paths.some((path) => /(^|\/)node_modules(\/|$)/.test(path)), 'pack must not include node_modules');
25
64
  assert.ok(packed[0].size < 30_000_000, `unexpectedly large package tarball: ${packed[0].size} bytes`);
26
65
 
27
66
  const install = join(temp, 'install');
28
- await import('node:fs/promises').then(({ mkdir }) => mkdir(install));
29
- run('npm', ['install', '--prefix', install, '--omit=dev', '--no-audit', '--no-fund', tarball]);
67
+ await mkdir(install);
68
+ run('npm', ['install', '--prefix', install, '--omit=dev', '--no-audit', '--no-fund', tarball], { env: npmEnv });
30
69
  const manifest = JSON.parse(await readFile(join(install, 'node_modules/carthagent/package.json'), 'utf8'));
31
- assert.equal(manifest.dependencies['@opencode/cli'], '2.0.32');
32
- const binary = join(install, 'node_modules/@opencode/cli/bin/opencode.exe');
33
- assert.ok((await stat(binary)).size > 100_000_000, 'native host binary must be materialized by postinstall');
34
- const version = run(binary, ['--version'], { cwd: install }).trim();
35
- assert.equal(version, 'opencode v2.0.32');
36
-
37
- const env = {
38
- ...process.env,
39
- CTG_OPENCODE_BIN: binary,
40
- CTG_EVALUATION_MAX_TURNS: '8',
41
- CTG_EVALUATION_MAX_COST: '1',
42
- HOME: join(temp, 'home'),
43
- XDG_CONFIG_HOME: join(temp, 'config'),
44
- XDG_DATA_HOME: join(temp, 'data'),
45
- XDG_STATE_HOME: join(temp, 'state'),
46
- XDG_CACHE_HOME: join(temp, 'cache'),
47
- };
48
70
  const installed = join(install, 'node_modules/carthagent');
49
- // The product entrypoint is intentionally parent-composed. Build a disposable
50
- // host entrypoint that composes the package's exported native integration.
51
- const installedPlugin = join(installed, '.ctg-production-plugin');
52
- await mkdir(installedPlugin);
53
- await writeFile(join(installedPlugin, 'index.ts'), `import { Plugin } from '@opencode/plugin';\nimport { setupCarthagent } from '../plugins/carthagent/src/index';\nexport default Plugin.define({ id: 'carthagent', setup: context => setupCarthagent(context, { delivery: true }) });\n`);
71
+ const installedPlugin = join(installed, 'plugins/carthagent');
72
+ assert.equal(manifest.devDependencies['@opencode/cli'], '2.0.32');
73
+ assert.equal(Object.hasOwn(manifest.dependencies, '@opencode/cli'), false, '@opencode/cli must not ship as a runtime dependency');
74
+ assert.equal(manifest.dependencies['@opencode/theme'], '2.0.32');
75
+ assert.equal(manifest.dependencies['@opentui/core'], '0.5.17');
76
+ assert.equal(manifest.dependencies['@opentui/solid'], '0.5.17');
77
+ assert.equal(manifest.dependencies['solid-js'], '1.9.12');
78
+ assert.equal(manifest.dependencies['web-tree-sitter'], '0.25.10');
79
+ assert.equal(manifest.dependencies['web-tree-sitter-legacy'], 'npm:web-tree-sitter@0.22.6');
80
+ const sharedRuntime = ['@opentui/core', '@opentui/solid', 'solid-js'];
81
+ for (const name of sharedRuntime) {
82
+ const hostResolution = createRequire(join(installed, 'package.json')).resolve(name);
83
+ const pluginResolution = createRequire(join(installedPlugin, 'package.json')).resolve(name);
84
+ assert.equal(pluginResolution, hostResolution, `${name} must resolve to the same host and plugin runtime instance`);
85
+ }
86
+ const packageVersion = async (from, name) => {
87
+ const entry = createRequire(from).resolve(name);
88
+ let directory = dirname(entry);
89
+ while (directory !== dirname(directory)) {
90
+ try {
91
+ const manifest = JSON.parse(await readFile(joinPath(directory, 'package.json'), 'utf8'));
92
+ if (manifest.name === name) return manifest.version;
93
+ } catch {}
94
+ directory = dirname(directory);
95
+ }
96
+ throw new Error(`Could not locate ${name} package manifest from ${entry}`);
97
+ };
98
+ assert.equal(await packageVersion(join(install, 'node_modules/@opentui/core/package.json'), 'web-tree-sitter'), '0.25.10');
99
+ assert.equal(await packageVersion(join(installed, 'package.json'), 'web-tree-sitter'), '0.25.10');
100
+ assert.equal(JSON.parse(await readFile(join(install, 'node_modules/web-tree-sitter-legacy/package.json'), 'utf8')).version, '0.22.6');
101
+ const releaseManifestPath = join(installed, 'lib/opencode/runtime-release.json');
102
+ let releaseManifest;
103
+ try { releaseManifest = JSON.parse(await readFile(releaseManifestPath, 'utf8')); }
104
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
105
+
106
+ // Ask the installed package's *default* resolver and identity reporter.
107
+ // The JSON explicitly distinguishes preview upstream-compatible packages
108
+ // from packages whose resolver acquired the pinned CTG-owned runtime.
109
+ const resolution = JSON.parse(run(process.execPath, ['--input-type=module', '-e', `
110
+ import { pathToFileURL } from 'node:url';
111
+ const runtime = await import(pathToFileURL(process.argv[1]).href);
112
+ const { loadNativeTestRuntime } = await import(pathToFileURL(process.argv[2]).href);
113
+ const options = {};
114
+ const binary = await runtime.resolveOpenCode(options);
115
+ const identity = await runtime.runtimeIdentity(binary, options);
116
+ const expectedManifest = JSON.parse(await (await import('node:fs/promises')).readFile(process.argv[3], 'utf8').catch(error => error.code === 'ENOENT' ? 'null' : Promise.reject(error)));
117
+ const { dirname, isAbsolute, relative, resolve, sep } = await import('node:path');
118
+ const ownedRoot = identity.kind === 'ctg-owned' ? resolve(dirname(dirname(dirname(binary)))) : undefined;
119
+ if (identity.kind === 'ctg-owned') {
120
+ const executableName = process.platform === 'win32' ? 'opencode.exe' : 'opencode';
121
+ const expectedBinary = resolve(ownedRoot, 'cli-' + identity.platform, 'bin', executableName);
122
+ if (binary !== expectedBinary) throw new Error('Selected owned runtime executable is outside its canonical artifact layout: expected ' + expectedBinary + ', got ' + binary);
123
+ }
124
+ const isolatedHome = process.env.HOME ?? process.env.USERPROFILE;
125
+ const cacheRelative = ownedRoot && relative(isolatedHome, ownedRoot);
126
+ if (ownedRoot && (!cacheRelative || cacheRelative === '..' || cacheRelative.startsWith('..' + sep) || isAbsolute(cacheRelative))) {
127
+ throw new Error('Default owned runtime cache escaped the isolated HOME: ' + ownedRoot);
128
+ }
129
+ const verified = await loadNativeTestRuntime({ binary, ownedRoot, expectedManifest });
130
+ console.log(JSON.stringify({ binary, ownedRoot: ownedRoot ?? null, identity, verified, cacheIsolated: Boolean(ownedRoot) }));
131
+ `, join(installed, 'lib/opencode/runtime.mjs'), join(installed, 'tools/native-runtime-identity.mjs'), releaseManifestPath], { cwd: install, env }).trim());
132
+ const binary = resolve(resolution.binary);
133
+ assert.ok((await stat(binary)).isFile(), 'default resolver must select a materialized runtime executable');
134
+ assert.ok(resolution.identity && typeof resolution.identity.kind === 'string', 'default resolver must report runtime identity');
135
+ if (resolution.identity.kind === 'ctg-owned') {
136
+ assert.ok(releaseManifest, 'owned runtime identity requires a package-pinned release manifest');
137
+ assert.ok(resolution.ownedRoot, 'owned runtime identity must report the acquired runtime root');
138
+ assert.equal(resolution.identity.version, releaseManifest.runtimeVersion, 'owned runtime version must match the package-pinned release');
139
+ assert.equal(resolution.identity.binaryPath && resolve(resolution.identity.binaryPath), binary, 'reported owned identity must name the selected executable');
140
+ assert.equal(resolution.verified.binary, binary, 'independent verifier must select the exact default-resolved executable');
141
+ assert.equal(resolution.verified.binarySha256, releaseManifest.assets[resolution.identity.platform].executableSha256,
142
+ 'independent verifier must match executable bytes against the installed trusted release manifest');
143
+ assert.equal(resolution.verified.sourceRevision, releaseManifest.sourceRevision, 'independent verifier must check runtime source provenance');
144
+ assert.equal(resolution.verified.patchSha256, releaseManifest.patchSha256, 'independent verifier must check runtime patch provenance');
145
+ assert.equal(resolution.verified.lockSha256, releaseManifest.lockSha256, 'independent verifier must check runtime lock provenance');
146
+ assert.equal(resolution.cacheIsolated, true, 'default acquisition cache must remain inside this test\'s isolated HOME');
147
+ } else {
148
+ assert.equal(resolution.identity.kind, 'upstream-compatible', 'unknown runtime identity cannot satisfy installed package acceptance');
149
+ assert.equal(resolution.identity.version, '2.0.32', 'preview upstream runtime must match the explicitly supported compatibility version');
150
+ assert.equal(resolution.verified.runtimeVersion, '2.0.32', 'independent verifier must validate upstream compatibility exactly');
151
+ assert.ok(!releaseManifest && !requireOwnedRuntime,
152
+ 'a release package manifest or --require-owned requires the default resolver to acquire its package-pinned CTG runtime; upstream-compatible mode is preview-only');
153
+ }
154
+
155
+ assert.throws(
156
+ () => createRequire(join(installed, 'package.json')).resolve('@opencode/cli/package.json'),
157
+ { code: 'MODULE_NOT_FOUND' },
158
+ 'installed package must not include the 203 MB OpenCode CLI fallback',
159
+ );
54
160
  // Run the acceptance harness from the installed artifact itself so its
55
161
  // imports (including @opencode/client) resolve only through that install.
56
- const composedPlugin = installedPlugin;
57
- for (const test of ['test-native-host.mjs', 'test-native-controls.mjs', 'test-native-outcomes.mjs', 'test-native-evaluation.mjs', 'test-native-swe.mjs']) {
58
- run(process.execPath, [join(installed, 'tools', test), composedPlugin], { cwd: install, env });
59
- }
60
- run(process.execPath, [join(installed, 'tools', 'test-native-swe-host.mjs'), join(installed, 'plugins/carthagent')], { cwd: install, env });
61
- for (const scenario of ['standard', 'strict', 'raw', 'budget', 'mixed', 'concurrent']) {
62
- run(process.execPath, [join(installed, 'tools', 'test-native-delivery-host.mjs'), composedPlugin], {
63
- cwd: install, env: { ...env, CTG_NATIVE_SCENARIO: scenario },
64
- });
162
+ const probes = [];
163
+ const acceptanceTests = [
164
+ 'test-native-ship-walkthrough.mjs', 'test-native-default-cli.mjs',
165
+ 'test-owned-host-controls.mjs', 'test-native-glyph-pty.mjs',
166
+ 'test-native-terminal-semantics-installed.mjs', 'test-native-visual-clear-installed.mjs',
167
+ 'test-native-host.mjs', 'test-native-ship-models.mjs', 'test-native-refresh-reentry.mjs',
168
+ 'test-native-controls.mjs', 'test-native-outcomes.mjs',
169
+ 'test-native-product.mjs', 'test-native-runtime.mjs', 'test-native-tasks.mjs',
170
+ 'test-native-review.mjs', 'test-native-swe-host.mjs', 'test-native-pty.mjs',
171
+ 'test-native-evaluation.mjs', 'test-native-swe.mjs',
172
+ 'test-native-report.mjs',
173
+ 'test-native-delivery-host.mjs',
174
+ '../tests/codegraph.test.mjs',
175
+ '../tests/native-cli-host-smoke.mjs',
176
+ ];
177
+ assert.ok(!focused || acceptanceTests.includes(focused), `Unknown installed acceptance probe: ${focused}`);
178
+ const tests = focused ? [focused] : acceptanceTests;
179
+ const cases = tests.flatMap(test => test === 'test-native-delivery-host.mjs'
180
+ ? ['standard', 'strict', 'budget', 'raw', 'mixed', 'concurrent'].map(scenario => ({ test, scenario }))
181
+ : [{ test }]);
182
+ for (const { test, scenario } of cases) {
183
+ const identity = { test, ...(scenario ? { scenario } : {}), runtime: resolution.identity };
184
+ const ownedProbe = ['test-owned-host-controls.mjs', 'test-native-glyph-pty.mjs',
185
+ 'test-native-terminal-semantics-installed.mjs', 'test-native-visual-clear-installed.mjs'].includes(test);
186
+ if (ownedProbe && resolution.identity.kind !== 'ctg-owned') {
187
+ const result = { ...identity, status: 'skipped', reason: 'owned-runtime probe is outside upstream-compatible preview scope' };
188
+ probes.push(result);
189
+ console.log(JSON.stringify(result));
190
+ continue;
191
+ }
192
+ if (ownedProbe && process.platform !== 'linux') {
193
+ assert.ok(!requireOwnedRuntime, `--require-owned cannot skip ${test} on ${process.platform}`);
194
+ const result = { ...identity, status: 'skipped', reason: 'owned installed acceptance probes currently require Linux PTY' };
195
+ probes.push(result);
196
+ console.log(JSON.stringify(result));
197
+ continue;
198
+ }
199
+ if (test === 'test-native-pty.mjs' && process.platform === 'win32') {
200
+ const result = { ...identity, status: 'skipped', reason: 'Unix-only Python PTY harness' };
201
+ probes.push(result);
202
+ console.log(JSON.stringify(result));
203
+ continue;
204
+ }
205
+ const started = performance.now();
206
+ console.log(JSON.stringify({ ...identity, status: 'started' }));
207
+ try {
208
+ const probePath = join(installed, 'tools', test);
209
+ // Parser/WASM behavior must resolve from the packed installation too.
210
+ const args = test.endsWith('.test.mjs') ? ['--test', probePath] : [probePath, installedPlugin];
211
+ if (test === 'test-owned-host-controls.mjs' || test === 'test-native-glyph-pty.mjs') args.push('--installed-default');
212
+ run(process.execPath, args, {
213
+ // The trusted harness may need to reacquire a missing default runtime.
214
+ // Runtime identity/version, host, PTY, workspace, and npm children
215
+ // independently strip the optional transport credential.
216
+ env: { ...env, CTG_NATIVE_SCENARIO: scenario ?? '', CTG_NATIVE_PLAN_PTY: scenario === 'standard' && resolution.identity.kind === 'ctg-owned' && process.platform === 'linux' ? '1' : '' },
217
+ cwd: install,
218
+ });
219
+ } catch (error) {
220
+ console.error(JSON.stringify({ ...identity, status: 'failed', elapsedMs: Math.round(performance.now() - started), completed: probes }));
221
+ throw error;
222
+ }
223
+ const result = { ...identity, status: 'passed', elapsedMs: Math.round(performance.now() - started) };
224
+ probes.push(result);
225
+ console.log(JSON.stringify(result));
65
226
  }
66
- run(process.execPath, [join(installed, 'tools', 'test-native-report.mjs'), join(installed, 'tools', 'native-report-plugin')], { cwd: install, env });
67
- console.log(JSON.stringify({ tarball: packed[0].filename, unpackedSize: packed[0].unpackedSize, binary, version }, null, 2));
227
+ console.log(JSON.stringify({
228
+ scope: focused ? 'focused' : 'full',
229
+ acceptance: resolution.identity.kind === 'ctg-owned' ? 'owned-runtime' : 'preview-upstream-compatible',
230
+ tarball: packed[0].filename,
231
+ unpackedSize: packed[0].unpackedSize,
232
+ runtime: resolution.identity,
233
+ binary,
234
+ probes,
235
+ }, null, 2));
68
236
  } finally {
69
237
  await rm(temp, { recursive: true, force: true });
70
238
  }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ /** Cached workspace RPC latency on the installed native host; not UI latency. */
3
+ import assert from 'node:assert/strict';
4
+ import { performance } from 'node:perf_hooks';
5
+ import { nativeFixture } from './native-fixture.mjs';
6
+
7
+ const host = await nativeFixture({ plugin: process.argv[2] });
8
+ try {
9
+ await host.active();
10
+ const snapshot = () => host.client.rpc.call({ rpcID: 'carthagent.workspace', method: 'snapshot',
11
+ input: {}, location: host.location }, host.request());
12
+ await snapshot();
13
+ const latencies = [];
14
+ for (let sample = 0; sample < 30; sample++) {
15
+ const started = performance.now();
16
+ const response = await snapshot();
17
+ latencies.push(performance.now() - started);
18
+ assert.equal(response.output.projects[0].directory, host.cwd);
19
+ }
20
+ const sorted = latencies.toSorted((a, b) => a - b);
21
+ const p95 = sorted[Math.ceil(sorted.length * 0.95) - 1];
22
+ assert.ok(p95 <= 300, `Cached workspace RPC p95 ${p95.toFixed(1)} ms exceeds 300 ms`);
23
+ console.log(JSON.stringify({ host: '2.0.32', samples: sorted.length,
24
+ cachedWorkspaceRpcP95Ms: Math.round(p95 * 10) / 10, maximumMs: Math.round(sorted.at(-1) * 10) / 10,
25
+ inputLatency: 'not measured by this probe', warmSwitchLatency: 'not measured by this probe' }));
26
+ } finally { await host.close(); }