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
@@ -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');
@@ -231,19 +256,10 @@ try {
231
256
  const health = await client.rpc.call({
232
257
  rpcID: 'carthagent', method: 'health', input: {}, location: { directory: cwd },
233
258
  }, request);
234
- console.log(JSON.stringify({ version: info.version, plugin: nativePlugin, health }, null, 2));
259
+ assert.equal(health.output.hostVersion, runtimeIdentity.runtimeVersion);
260
+ console.log(JSON.stringify({ version: info.version, compatibilityVersion: runtimeIdentity.compatibilityVersion, plugin: nativePlugin, health }, null, 2));
235
261
  assert.equal(health.output.directory, cwd);
236
262
  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
263
  const echo = await client.rpc.call({
248
264
  rpcID: 'carthagent', method: 'echo', input: { text: 'native RPC fixture' }, location: { directory: cwd },
249
265
  }, request);
@@ -303,7 +319,7 @@ try {
303
319
  attached.once('close', (code, signal) => {
304
320
  clearTimeout(timer);
305
321
  if (code === 0) resolveRun(stdout);
306
- else reject(new Error(`Attached native CLI exited ${code ?? signal}: ${stderr}`));
322
+ else reject(new Error(`Attached native CLI exited ${code ?? signal}: ${stderr}\n${stdout}`));
307
323
  });
308
324
  });
309
325
  assert.match(attachedRun, /CTG_FIXTURE_OK/, 'the packaged CLI should attach to and prompt the ensured service');
@@ -327,6 +343,16 @@ try {
327
343
  } while (Date.now() < ptyDeadline);
328
344
  assert.equal(ptyState.data.status, 'running', 'native TUI should attach and remain active in the service-owned PTY');
329
345
  await client.pty.remove({ ptyID, location: { directory: cwd } }, request);
346
+ const auth = await client.rpc.call({
347
+ rpcID: 'carthagent', method: 'authStart', input: {}, location: { directory: cwd },
348
+ }, request);
349
+ assert.equal(auth.output.userCode, 'OFFLINE');
350
+ assert.match(auth.output.flowId, /^[0-9a-f-]{36}$/);
351
+ assert.match(deviceAuthorizationBody.installId, /^[0-9a-f-]{36}$/);
352
+ assert.equal((await client.rpc.call({
353
+ rpcID: 'carthagent', method: 'authCancel', input: { flowId: auth.output.flowId }, location: { directory: cwd },
354
+ }, request)).output.cancelled, true);
355
+ console.log('Device authorization start/cancel RPC passed against offline OpenCode host.');
330
356
  await assert.rejects(client.rpc.call({
331
357
  rpcID: 'carthagent', method: 'health', input: { unexpected: true }, location: { directory: cwd },
332
358
  }, request));
@@ -350,14 +376,14 @@ try {
350
376
  await assert.rejects(archiveCall('search', { query: ' ' }), (error) => error.type === 'archive_error');
351
377
  await assert.rejects(archiveCall('list', { limit: 101 }), (error) => error.type === 'rpc.invalid_input');
352
378
  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.');
379
+ console.log(`Native plugin, archive RPC methods, output schemas, and declared failures passed against OpenCode ${runtimeIdentity.runtimeVersion} (compatibility ${runtimeIdentity.compatibilityVersion}).`);
354
380
  } catch (error) {
355
381
  console.error(JSON.stringify({ fixtureRequests }));
356
382
  console.error(output.replaceAll(password, '[redacted]'));
357
383
  throw error;
358
384
  } finally {
359
385
  if (child) {
360
- const closed = childClosed ? Promise.resolve() : once(child, 'close').then(() => { childClosed = true; });
386
+ const closed = childClosed ? Promise.resolve() : once(child, 'close');
361
387
  if (child.exitCode === null && child.signalCode === null) {
362
388
  if (process.platform === 'win32') {
363
389
  // This PID is the exact host process spawned above; /T is needed to
@@ -376,10 +402,8 @@ try {
376
402
  await closed;
377
403
  }
378
404
  }
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
- ]);
405
+ await new Promise((resolveClose) => fixture.close(resolveClose));
406
+ await new Promise((resolveClose) => authServer.close(resolveClose));
407
+ await new Promise((resolveClose) => gatewayServer.close(resolveClose));
384
408
  await rm(temp, { recursive: true, force: true, maxRetries: 12, retryDelay: 250 });
385
409
  }
@@ -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(); }