carthagent 1.0.7 → 1.1.0-preview.12

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 (275) hide show
  1. package/README.md +80 -79
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  4. package/docs/glyph-review/test-isolation-v1.md +26 -0
  5. package/docs/glyph-review/typecheck-base.log +1 -0
  6. package/docs/glyph-review/typecheck-v4.log +1 -0
  7. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  8. package/docs/help-dispatch-review.md +69 -0
  9. package/docs/legacy-cli.md +150 -0
  10. package/docs/live-theme-plugin-api.md +18 -0
  11. package/docs/native-cli.md +102 -0
  12. package/docs/native-compatibility.md +16 -0
  13. package/docs/native-daily-quests.md +132 -0
  14. package/docs/native-glyph-pty.md +59 -0
  15. package/docs/native-manual-testing.md +314 -0
  16. package/docs/native-slash-acceptance-handoff.md +49 -0
  17. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  18. package/docs/native-terminal-performance.md +310 -0
  19. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  20. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  21. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  22. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  23. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  24. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  31. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  32. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  33. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  35. package/docs/native-terminal-semantics.md +89 -0
  36. package/docs/native-upgrade.md +177 -0
  37. package/docs/opencode-composed-source-review.md +89 -0
  38. package/docs/opencode-migration.md +225 -9
  39. package/docs/opencode-owned-runtime.md +241 -0
  40. package/docs/opencode-runtime.md +12 -0
  41. package/docs/opencode-theme-provider-proof/README.md +55 -0
  42. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  43. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  44. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  45. package/docs/owned-host-controls-proof.md +39 -0
  46. package/docs/owned-runtime-release-manifest.md +42 -0
  47. package/docs/owned-visual-clear-proof.md +40 -0
  48. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  49. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  50. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  51. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  52. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  53. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  54. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  57. package/docs/release-notes-native-draft.md +73 -0
  58. package/docs/release-notes-preview.10.md +121 -0
  59. package/docs/release-notes-preview.11.md +76 -0
  60. package/docs/release-notes-preview.12.md +55 -0
  61. package/docs/release-notes-preview.9.md +130 -0
  62. package/evaluate.mjs +28 -1
  63. package/lib/budget.mjs +1 -1
  64. package/lib/codegraph.mjs +1 -1
  65. package/lib/delivery.mjs +3 -2
  66. package/lib/environment.mjs +8 -0
  67. package/lib/guidance.mjs +1 -1
  68. package/lib/opencode/cli-runtime.mjs +1171 -0
  69. package/lib/opencode/cli.mjs +252 -0
  70. package/lib/opencode/credentials.mjs +63 -8
  71. package/lib/opencode/demo.mjs +53 -0
  72. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  73. package/lib/opencode/evaluation.mjs +32 -12
  74. package/lib/opencode/headless.mjs +8 -4
  75. package/lib/opencode/provider-policy.mjs +23 -0
  76. package/lib/opencode/review.mjs +37 -16
  77. package/lib/opencode/runtime-artifact.mjs +335 -0
  78. package/lib/opencode/runtime-github-transport.mjs +121 -0
  79. package/lib/opencode/runtime-release.json +36 -0
  80. package/lib/opencode/runtime.mjs +298 -20
  81. package/lib/opencode/service.mjs +77 -35
  82. package/lib/opencode/session-options.mjs +6 -2
  83. package/lib/opencode/update.mjs +222 -0
  84. package/lib/opencode/workspace.mjs +1 -1
  85. package/lib/planning-advice.mjs +2 -2
  86. package/lib/tui/framing.mjs +1 -80
  87. package/lib/update.mjs +10 -1
  88. package/package.json +33 -7
  89. package/plugins/carthagent/README.md +23 -0
  90. package/plugins/carthagent/src/bootstrap.ts +1 -1
  91. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  92. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  93. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  94. package/plugins/carthagent/src/index.ts +71 -24
  95. package/plugins/carthagent/src/rpc.ts +51 -6
  96. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  97. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  98. package/plugins/carthagent/src/server/daily-source-rpc.ts +197 -0
  99. package/plugins/carthagent/src/server/delivery.ts +217 -106
  100. package/plugins/carthagent/src/server/framing.mjs +80 -0
  101. package/plugins/carthagent/src/server/policy.ts +1 -1
  102. package/plugins/carthagent/src/server/providers.ts +58 -30
  103. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  104. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  105. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  106. package/plugins/carthagent/src/server/ship-guidance.ts +39 -0
  107. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  108. package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
  109. package/plugins/carthagent/src/testing.ts +5 -0
  110. package/plugins/carthagent/src/tui.tsx +492 -0
  111. package/plugins/carthagent/src/ui/client.ts +125 -0
  112. package/plugins/carthagent/src/ui/commands.ts +70 -0
  113. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  114. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  115. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  116. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  117. package/plugins/carthagent/src/ui/daily-quests.tsx +232 -0
  118. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  119. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  120. package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
  121. package/plugins/carthagent/src/ui/native-review.mjs +154 -0
  122. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  123. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  124. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  125. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  126. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  127. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  128. package/plugins/carthagent/src/ui/voyages-navigation.tsx +122 -0
  129. package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
  130. package/plugins/carthagent/src/ui/voyages.tsx +127 -0
  131. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  132. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  133. package/plugins/carthagent/src/voyages-rpc.ts +60 -0
  134. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  135. package/plugins/carthagent/tests/commands.test.ts +107 -0
  136. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  137. package/plugins/carthagent/tests/voyages-navigation.test.tsx +258 -0
  138. package/plugins/carthagent/themes/carthage.json +67 -0
  139. package/plugins/carthagent/tui.ts +26 -1
  140. package/script/ctg/build-owned-runtime.sh +166 -0
  141. package/tests/budget-engine.test.mjs +4 -3
  142. package/tests/cli.test.mjs +52 -96
  143. package/tests/daily-evidence.test.mjs +62 -0
  144. package/tests/delivery.test.mjs +22 -1
  145. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  146. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  147. package/tests/installed-runtime-contract.test.mjs +43 -0
  148. package/tests/native-cli-host-smoke.mjs +457 -0
  149. package/tests/native-daily-editor-focus.test.mjs +213 -0
  150. package/tests/native-daily-editor.test.mjs +43 -0
  151. package/tests/native-daily-quests-layout.test.mjs +53 -0
  152. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  153. package/tests/native-daily-quests-ui.test.mjs +778 -0
  154. package/tests/native-daily-source.test.mjs +426 -0
  155. package/tests/native-demo-ui.test.mjs +48 -0
  156. package/tests/native-evaluation-admission.test.mjs +24 -0
  157. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  158. package/tests/native-fixture-runtime.test.mjs +111 -0
  159. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  160. package/tests/native-glyph-pty.test.mjs +107 -0
  161. package/tests/native-manual-projects.test.mjs +170 -0
  162. package/tests/native-owned-runtime-env.test.mjs +140 -0
  163. package/tests/native-paired-guards.test.mjs +464 -0
  164. package/tests/native-provider-policy.test.mjs +31 -0
  165. package/tests/native-pty-invocation.test.mjs +114 -0
  166. package/tests/native-pty-runtime.test.mjs +45 -0
  167. package/tests/native-resume-session.test.mjs +46 -0
  168. package/tests/native-review-consumer.test.mjs +267 -0
  169. package/tests/native-runtime-identity.test.mjs +214 -0
  170. package/tests/native-ship-parity.test.mjs +526 -0
  171. package/tests/native-ship-plan.test.mjs +75 -0
  172. package/tests/native-ship-rpc.test.mjs +71 -0
  173. package/tests/native-ui-commands.test.mjs +6 -0
  174. package/tests/native-voyages-layout.test.mjs +28 -0
  175. package/tests/native-voyages-navigation.test.mjs +169 -0
  176. package/tests/native-voyages-rpc.test.mjs +395 -0
  177. package/tests/native-voyages-ui.test.mjs +277 -0
  178. package/tests/opencode-atomic-write.test.mjs +55 -0
  179. package/tests/opencode-cli.test.mjs +1312 -0
  180. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  181. package/tests/opencode-providers.test.mjs +245 -18
  182. package/tests/opencode-runtime.test.mjs +546 -8
  183. package/tests/opencode-session-controls.test.mjs +15 -0
  184. package/tests/opencode-ship-rpc.test.mjs +71 -0
  185. package/tests/opencode-update.test.mjs +475 -0
  186. package/tests/opencode-workspace.test.mjs +51 -2
  187. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  188. package/tests/release-workflow.test.mjs +46 -0
  189. package/tests/runtime-artifact.test.mjs +269 -0
  190. package/tests/runtime-development-trust.test.mjs +230 -0
  191. package/tests/runtime-github-transport.test.mjs +132 -0
  192. package/tests/runtime-token-env.test.mjs +23 -0
  193. package/tests/test_native_terminal_probe.py +76 -0
  194. package/tests/test_native_terminal_semantics.py +232 -0
  195. package/tests/update.test.mjs +36 -0
  196. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  197. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  198. package/tests/workspace-live.test.mjs +127 -0
  199. package/tools/benchmark/diagnose-request.mjs +59 -36
  200. package/tools/benchmark/eval-common.mjs +3 -3
  201. package/tools/benchmark/evaluators.test.mjs +4 -4
  202. package/tools/benchmark/guard.test.mjs +35 -2
  203. package/tools/benchmark/guard.ts +20 -7
  204. package/tools/benchmark/planning-startup.test.mjs +30 -34
  205. package/tools/benchmark/runner.mjs +12 -7
  206. package/tools/benchmark/runner.test.mjs +23 -7
  207. package/tools/benchmark/upstream-error.test.mjs +16 -2
  208. package/tools/generate-owned-runtime-release.mjs +217 -0
  209. package/tools/installed-runtime-contract.mjs +16 -0
  210. package/tools/manual-runtime-manifest.mjs +39 -0
  211. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  212. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  213. package/tools/native-fixture.mjs +199 -40
  214. package/tools/native-glyph-helpers.py +127 -0
  215. package/tools/native-glyph-pty.py +144 -0
  216. package/tools/native-manual-projects.mjs +259 -0
  217. package/tools/native-manual.mjs +338 -0
  218. package/tools/native-model-picker-acceptance.py +306 -0
  219. package/tools/native-owned-runtime-env.sh +47 -0
  220. package/tools/native-paired-guards.mjs +282 -0
  221. package/tools/native-paired-manifest.mjs +72 -0
  222. package/tools/native-paired-orchestration.sh +51 -0
  223. package/tools/native-plugin-readiness.mjs +33 -0
  224. package/tools/native-plugin-readiness.test.mjs +53 -0
  225. package/tools/native-pty-invocation.mjs +26 -0
  226. package/tools/native-pty-runtime.mjs +83 -0
  227. package/tools/native-pty-smoke.py +147 -24
  228. package/tools/native-public-repo-shell-result.mjs +13 -0
  229. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  230. package/tools/native-runtime-identity.mjs +201 -0
  231. package/tools/native-slash-acceptance-env.py +38 -0
  232. package/tools/native-terminal-semantics.py +876 -0
  233. package/tools/owned-shell-observer/index.ts +16 -0
  234. package/tools/owned-shell-observer/package.json +1 -0
  235. package/tools/owned-visual-clear-env.py +48 -0
  236. package/tools/owned-visual-clear-model.py +21 -0
  237. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  238. package/tools/prepare-native-manual.mjs +122 -0
  239. package/tools/run-native-terminal-paired.sh +403 -0
  240. package/tools/runtime-token-env.mjs +8 -0
  241. package/tools/seed-native-switch-fixture.mjs +79 -0
  242. package/tools/test-native-controls.mjs +3 -1
  243. package/tools/test-native-daily-quests.mjs +65 -0
  244. package/tools/test-native-default-cli.mjs +106 -0
  245. package/tools/test-native-delivery-host.mjs +100 -18
  246. package/tools/test-native-evaluation.mjs +7 -3
  247. package/tools/test-native-glyph-pty.mjs +94 -0
  248. package/tools/test-native-host.mjs +83 -61
  249. package/tools/test-native-outcomes.mjs +3 -1
  250. package/tools/test-native-package.mjs +206 -38
  251. package/tools/test-native-performance.mjs +26 -0
  252. package/tools/test-native-product.mjs +90 -0
  253. package/tools/test-native-pty.mjs +34 -9
  254. package/tools/test-native-public-repo.mjs +224 -0
  255. package/tools/test-native-refresh-reentry.mjs +106 -0
  256. package/tools/test-native-report.mjs +27 -20
  257. package/tools/test-native-review.mjs +75 -0
  258. package/tools/test-native-runtime.mjs +120 -0
  259. package/tools/test-native-ship-models.mjs +176 -0
  260. package/tools/test-native-ship-walkthrough.mjs +96 -0
  261. package/tools/test-native-slash-commands.mjs +125 -0
  262. package/tools/test-native-slash-commands.py +242 -0
  263. package/tools/test-native-swe-host.mjs +224 -55
  264. package/tools/test-native-tasks.mjs +77 -0
  265. package/tools/test-native-terminal-performance.py +887 -0
  266. package/tools/test-native-terminal-readiness.sh +85 -0
  267. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  268. package/tools/test-native-visual-clear-installed.mjs +48 -0
  269. package/tools/test-owned-host-controls.mjs +238 -0
  270. package/tools/test-owned-opencode-runtime.mjs +35 -0
  271. package/tools/test-owned-visual-clear-env.py +120 -0
  272. package/tools/test-owned-visual-clear.py +373 -0
  273. package/tools/verify-draft-runtime-gate.mjs +138 -0
  274. package/tools/verify-owned-runtime-release.mjs +114 -0
  275. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /** Integration entrypoint probe: all handlers must load from the shipped plugin. */
3
+ import assert from 'node:assert/strict';
4
+ import { nativeFixture } from './native-fixture.mjs';
5
+
6
+ const host = await nativeFixture({ plugin: process.argv[2] });
7
+ const identity = host.runtimeIdentity;
8
+ let stream = new AbortController();
9
+ let watching;
10
+ try {
11
+ const { client, location, model, request } = host;
12
+ await host.active();
13
+ const rpc = async (rpcID, method, input) => (await client.rpc.call({ rpcID, method, input, location }, request())).output;
14
+ const session = await client.session.create({ location, model, title: 'Product entrypoint probe' }, request());
15
+ const health = await rpc('carthagent', 'health', {});
16
+ assert.equal(health.directory, host.cwd);
17
+ assert.equal(health.hostVersion, identity.runtimeVersion);
18
+ const snapshot = await rpc('carthagent.workspace', 'snapshot', {});
19
+ assert.equal(snapshot.projects.length, 1);
20
+ assert.equal(snapshot.projects[0].id, session.projectID);
21
+ assert.equal(snapshot.projects[0].directory, host.cwd);
22
+ assert.deepEqual(snapshot.tasks, []);
23
+ const opened = await rpc('carthagent.workspace', 'open', { sessionID: session.id });
24
+ assert.deepEqual(opened, { opened: true, directory: host.cwd });
25
+ assert.deepEqual(await rpc('carthagent.workspace', 'delivery', { sessionID: session.id }), { status: 'unavailable' });
26
+ const archives = await rpc('carthagent.archive', 'list', {});
27
+ assert.deepEqual(archives.items, []);
28
+ assert.equal(archives.total, 0);
29
+ assert.deepEqual(await rpc('carthagent.session', 'framing', { sessionID: session.id, enabled: false }), { enabled: false });
30
+ let connected;
31
+ const ready = new Promise(resolve => { connected = resolve; });
32
+ let changed;
33
+ const updated = new Promise(resolve => { changed = resolve; });
34
+ watching = (async () => {
35
+ try {
36
+ for await (const event of client.event.subscribe({ signal: stream.signal })) {
37
+ if (event.type === 'server.connected') connected();
38
+ if (event.type === 'rpc.carthagent.workspace.updated' && event.location?.directory === host.cwd) changed(event);
39
+ }
40
+ } catch (error) { if (!stream.signal.aborted) throw error; }
41
+ })();
42
+ let timer;
43
+ const timed = new Promise((_, reject) => { timer = setTimeout(() => reject(Error('Workspace event stream did not reconcile')), 10_000); });
44
+ try {
45
+ await Promise.race([ready, timed]);
46
+ await client.session.prompt({ sessionID: session.id, text: 'Fixture event refresh' }, request());
47
+ await client.session.wait({ sessionID: session.id }, request());
48
+ const event = await Promise.race([updated, timed]);
49
+ assert.equal(typeof event.data.revision, 'string');
50
+ } finally { clearTimeout(timer); }
51
+ // The native event stream is live-only. Drop the subscriber, perform a whole
52
+ // turn, then recover from authoritative state before watching future events.
53
+ stream.abort();
54
+ await watching;
55
+ await client.session.prompt({ sessionID: session.id, text: 'Completed while disconnected' }, request());
56
+ assert.equal(await host.outcome(session.id), 'succeeded');
57
+ stream = new AbortController();
58
+ const reconnected = new Promise(resolve => { connected = resolve; });
59
+ const refreshed = new Promise(resolve => { changed = resolve; });
60
+ watching = (async () => {
61
+ try {
62
+ for await (const event of client.event.subscribe({ signal: stream.signal })) {
63
+ if (event.type === 'server.connected') connected();
64
+ if (event.type === 'rpc.carthagent.workspace.updated' && event.location?.directory === host.cwd) changed(event);
65
+ }
66
+ } catch (error) { if (!stream.signal.aborted) throw error; }
67
+ })();
68
+ const reconnectTimeout = new Promise((_, reject) => { timer = setTimeout(() => reject(Error('Reconnected workspace events did not refresh')), 10_000); });
69
+ try {
70
+ await Promise.race([reconnected, reconnectTimeout]);
71
+ const recovered = await client.session.get({ sessionID: session.id }, request());
72
+ assert.equal(recovered.outcome, 'succeeded');
73
+ const context = await client.session.context({ sessionID: session.id }, request());
74
+ assert.ok(context.some(message => message.type === 'user' && message.text === 'Completed while disconnected'));
75
+ assert.deepEqual(await rpc('carthagent.workspace', 'open', { sessionID: session.id }), opened);
76
+ await client.session.prompt({ sessionID: session.id, text: 'Turn after reconnect' }, request());
77
+ assert.equal(await host.outcome(session.id), 'succeeded');
78
+ assert.equal(typeof (await Promise.race([refreshed, reconnectTimeout])).data.revision, 'string');
79
+ } finally { clearTimeout(timer); }
80
+ console.log(JSON.stringify({ host: identity.runtimeVersion, compatibilityVersion: identity.compatibilityVersion,
81
+ ...(identity.binarySha256 ? { binarySha256: identity.binarySha256 } : {}),
82
+ entrypoint: 'passed', workspace: 'passed', archive: 'passed', controls: 'passed', eventInvalidation: 'passed', reconnectSnapshot: 'passed', reconnectEvents: 'passed' }));
83
+ } catch (error) {
84
+ console.error(host.output);
85
+ throw error;
86
+ } finally {
87
+ stream.abort();
88
+ try { await watching; }
89
+ finally { await host.close(); }
90
+ }
@@ -1,20 +1,45 @@
1
1
  #!/usr/bin/env node
2
- /** Unix-only PTY helper; CI skips this test explicitly on Windows. */
2
+ /** Unix-only command and native-theme smoke tests in an isolated OpenCode PTY. */
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import { resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { createHash } from 'node:crypto';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { resolveOpenCode, runtimeIdentity } from '../lib/opencode/runtime.mjs';
9
+ import { loadNativeTestRuntime, preflightNativeTestRuntimeArtifact } from './native-runtime-identity.mjs';
10
+ import { selectNativePtyRuntime } from './native-pty-runtime.mjs';
11
+ import { nativePtySmokeArgs } from './native-pty-invocation.mjs';
6
12
 
7
13
  if (process.platform === 'win32') {
8
14
  console.log('Native PTY smoke skipped: the Python PTY implementation is Unix-only.');
9
15
  process.exit(0);
10
16
  }
17
+
11
18
  const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
12
- const binary = process.env.CTG_OPENCODE_BIN ?? resolve(root, 'node_modules/@opencode/cli/bin/opencode.exe');
13
- const plugin = resolve(root, 'plugins/carthagent');
14
- const testPlugin = resolve(plugin, 'test-plugin');
15
- const result = spawnSync('python3', [resolve(root, 'tools/native-pty-smoke.py'), '--binary', binary, '--plugin', plugin, '--delivery-plugin', testPlugin], {
16
- cwd: root,
17
- stdio: 'inherit',
19
+ const selection = await selectNativePtyRuntime({
20
+ runtime: { resolveOpenCode, runtimeIdentity },
21
+ root,
22
+ identityTools: { loadNativeTestRuntime, preflightNativeTestRuntimeArtifact },
18
23
  });
19
- if (result.error) throw result.error;
20
- if (result.status !== 0) process.exit(result.status ?? 1);
24
+ const binary = selection.binary;
25
+ const plugin = resolve(process.argv[2] ?? resolve(root, 'plugins/carthagent'));
26
+ const probes = [
27
+ { command: '/ctg-status', expect: `Carthagent ${selection.identity.version}` },
28
+ { command: '/ctg-workspace', expect: 'Workspace native sessions', postSubmitMarker: 'Workspace native sessions', dismissCompletion: true },
29
+ { command: '/help', expect: 'Carthagent commands', postSubmitMarker: '(/ctg-help)', dismissCompletion: true },
30
+ { command: '/ctg-help', expect: 'Carthagent commands', dismissCompletion: true },
31
+ { command: '/compact', expect: '/compact requires an open session.', secondEnter: true, dismissCompletion: true },
32
+ {
33
+ command: '/theme carthage', expect: 'Theme “carthage” saved', postSubmitMarker: 'Carthagent reloads valid theme selections', expectedTheme: 'carthage', dismissCompletion: true,
34
+ },
35
+ { command: '/ctg-status', expect: `Carthagent ${selection.identity.version}`, initialTheme: 'carthage', color: '213,175,102' },
36
+ ];
37
+
38
+ for (const probe of probes) {
39
+ const currentSha256 = createHash('sha256').update(await readFile(binary)).digest('hex');
40
+ if (currentSha256 !== selection.binarySha256) throw new Error('Selected OpenCode binary changed after native runtime verification.');
41
+ const args = nativePtySmokeArgs({ root, binary, plugin, selection, probe });
42
+ const result = spawnSync('python3', args, { cwd: root, stdio: 'inherit', timeout: 90_000 });
43
+ if (result.error) throw result.error;
44
+ if (result.status !== 0) process.exit(result.status ?? 1);
45
+ }
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ /** Offline native delivery against a clean checkout of a pinned public JavaScript repository. */
3
+ import assert from 'node:assert/strict';
4
+ import { createHash } from 'node:crypto';
5
+ import { execFileSync, spawnSync } from 'node:child_process';
6
+ import { cp, copyFile, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { withoutRuntimeGithubToken } from '../lib/environment.mjs';
10
+ import { nativeFixture } from './native-fixture.mjs';
11
+ import { runNativeEvaluation } from '../lib/opencode/evaluation.mjs';
12
+ import { assertNativeShellResult } from './native-public-repo-shell-result.mjs';
13
+
14
+ const repository = 'https://github.com/jonschlinkert/is-number.git';
15
+ const commit = '98e8ff1da1a89f93d1397a24d7413ed15421c139';
16
+ const license = 'MIT';
17
+ const proofDirectory = process.env.CTG_PUBLIC_REPO_PROOF_DIR;
18
+ const childEnv = withoutRuntimeGithubToken(process.env);
19
+ let sourceDirectory;
20
+ const root = join(import.meta.dirname, '..');
21
+ const packageVersion = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')).version;
22
+ const testFile = `const test = require('node:test');\nconst assert = require('node:assert/strict');\nconst isNumber = require('./');\ntest('recognizes bigint primitives without changing finite-number behavior', () => {\n assert.equal(isNumber(42n), true);\n assert.equal(isNumber(-1n), true);\n assert.equal(isNumber(1.5), true);\n assert.equal(isNumber('1.5'), true);\n assert.equal(isNumber(Infinity), false);\n});\n`;
23
+ const host = await nativeFixture({
24
+ plugin: join(root, 'plugins/carthagent'),
25
+ plugins: [join(root, 'lib/opencode/evaluation-budget-plugin')],
26
+ env: { CTG_EVALUATION_MAX_TURNS: '24', CTG_EVALUATION_MAX_COST: '1', CTG_SESSION_OPTIONS: JSON.stringify({ maxTools: 50, maxSeconds: 180, maxRepairs: 12 }) },
27
+ respond(body, res) {
28
+ const messages = body.messages ?? [];
29
+ const tools = (body.tools ?? []).map(tool => tool.function?.name);
30
+ const calls = messages.flatMap(message => message.tool_calls ?? []);
31
+ const results = new Map(messages.filter(message => message.role === 'tool').map(message => [message.tool_call_id, String(message.content)]));
32
+ const completed = calls.filter(call => results.has(call.id)).map(call => ({ ...call, output: results.get(call.id) }));
33
+ const execute = completed.filter(call => call.function?.name === 'execute');
34
+ const code = call => { try { return JSON.parse(call.function.arguments ?? '{}').code ?? ''; } catch { return ''; } };
35
+ const did = name => execute.some(call => code(call).includes(name));
36
+ const dispatch = (name, args) => {
37
+ if (!tools.includes(name)) throw Error(`Required native tool ${name} was not advertised: ${tools.join(', ')}`);
38
+ const call = { index: 0, id: `public-repo-${host.requests.length}`, type: 'function', function: { name, arguments: JSON.stringify(args) } };
39
+ const chunk = (delta, finish_reason = null, usage) => `data: ${JSON.stringify({ id: `public-repo-${host.requests.length}`, object: 'chat.completion.chunk', created: 1, model: 'fixture', choices: [{ index: 0, delta, finish_reason }], ...(usage ? { usage } : {}) })}\n\n`;
40
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
41
+ res.end(`${chunk({ role: 'assistant', tool_calls: [call] })}${chunk({}, 'tool_calls', { prompt_tokens: 20, completion_tokens: 8, total_tokens: 28 })}data: [DONE]\n\n`);
42
+ };
43
+ const execution = code => dispatch('execute', { code });
44
+ const shells = completed.filter(call => call.function?.name === 'shell');
45
+ const shell = () => {
46
+ const command = shells.length % 2 === 0 ? 'npm test -- --recursive test.js' : `${JSON.stringify(process.execPath)} --test ctg-bigint.test.cjs`;
47
+ dispatch('shell', { command: `${command}; ctg_status=$?; printf '\\nCTG_NATIVE_EXIT_STATUS=%s\\n' "$ctg_status"; exit "$ctg_status"`, workdir: host.cwd });
48
+ };
49
+ if (!completed.length) return execution('const result = await search({query:"CTG delivery plan acceptance checks", namespace:"carthagent", limit:20}); return result');
50
+ if (!did('delivery_plan')) return execution(`const result = await tools.carthagent["delivery_plan"]({goal:"Recognize bigint primitives in the pinned is-number package",verification:"required",assumptions:["Bigint primitives are numeric values and do not require coercion."],artifacts:["."],outputs:["index.js"],steps:[{id:"bigint",title:"Add bigint primitive support",dependsOn:[],checks:["package-tests","bigint-tests"]}],acceptance:[{requirement:"Bigint primitives are recognized while existing finite number and string behavior remains unchanged.",checks:["package-tests","bigint-tests"]}],checks:[{id:"package-tests",kind:"test",argv:["npm","test","--","--recursive","test.js"],timeoutSeconds:60},{id:"bigint-tests",kind:"test",argv:[${JSON.stringify(process.execPath)},"--test","ctg-bigint.test.cjs"],timeoutSeconds:30}]}); return {marker:"PUBLIC_REPO_PLAN",result}`);
51
+ const planResult = completed.find(call => call.function?.name === 'execute' && code(call).includes('delivery_plan'))?.output ?? '';
52
+ if (!tools.includes('shell')) throw Error(`Delivery plan did not unlock shell; plan response: ${planResult}`);
53
+ if (shells.length < 2) return shell();
54
+ if (shells.length === 2) {
55
+ assertNativeShellResult(shells[0].output, { status: 0, passingTests: 111 });
56
+ assertNativeShellResult(shells[1].output, { status: 1 });
57
+ assert.match(shells[1].output, /\b(?:fail|not ok)\s+1\b/i, 'bigint regression must show its expected red assertion');
58
+ if (!completed.some(call => call.function?.name === 'edit')) {
59
+ if (!tools.includes('edit')) throw Error(`Plan did not unlock native edit; advertised: ${tools.join(', ')}`);
60
+ dispatch('edit', { path: join(host.cwd, 'index.js'), oldString: " if (typeof num === 'number') {\n return num - num === 0;\n }", newString: " if (typeof num === 'bigint') {\n return true;\n }\n if (typeof num === 'number') {\n return num - num === 0;\n }" });
61
+ return;
62
+ }
63
+ return shell();
64
+ }
65
+ if (shells.length < 4) return shell();
66
+ assertNativeShellResult(shells[2].output, { status: 0, passingTests: 111 });
67
+ assertNativeShellResult(shells[3].output, { status: 0 });
68
+ assert.match(shells[3].output, /pass\s+1\b/i, 'bigint regression must pass after edit');
69
+ if (!did('delivery_check')) return execution('return await tools.carthagent["delivery_check"]({id:"all"})');
70
+ if (!did('delivery_progress')) return execution('const result = await tools.carthagent["delivery_progress"]({step:"bigint",status:"done",note:"Pinned public package baseline test and bigint regression test pass after the native edit."}); return result');
71
+ const review = [...execute].reverse().find(call => /delivery_review/.test(code(call)) && /inspect/.test(code(call)));
72
+ if (!review) return execution('return await tools.carthagent["delivery_review"]({action:"inspect"})');
73
+ const id = review.output.match(/(?:"captureId"|"id")\s*:\s*"([^"]+)"/)?.[1];
74
+ if (!id) throw Error(`Review capture ID missing in inspect tool output: ${review.output}`);
75
+ if (!completed.some(call => call.function?.name === 'execute' && /delivery_review/.test(code(call)) && /record/.test(code(call)))) {
76
+ return execution(`const result=await tools.carthagent["delivery_review"]({action:"record",captureId:${JSON.stringify(id)},coverage:[{requirement:"Bigint primitives are recognized while existing finite number and string behavior remains unchanged.",assertions:"npm test and node --test ctg-bigint.test.cjs pass in the pinned checkout.",status:"demonstrated"}],probes:["The added test failed on the unmodified public source.","After the native edit, package tests and added regression test pass."],findings:[],limitations:["Offline deterministic provider drives tool calls; no independent code reviewer or public hosting was used.","The task tests an isolated feature addition, not upstream maintainership or a production release."]}); return result`);
77
+ }
78
+ if (!completed.some(call => call.function?.name === 'execute' && /delivery_finish/.test(code(call)) && /Pinned package test suite/.test(code(call)))) return execution('const result=await tools.carthagent["delivery_finish"]({status:"verified",review:"Pinned package test suite and separate declared bigint regression check passed; coverage and probes recorded.",launch:"Not applicable to this library change.",limitations:["Offline deterministic provider; no independent reviewer or public repository write.","One small feature addition in a pinned checkout is not a full migration acceptance claim."]}); return result');
79
+ const chunk = (delta, finish_reason = null) => `data: ${JSON.stringify({ id: `public-repo-${host.requests.length}`, object: 'chat.completion.chunk', created: 1, model: 'fixture', choices: [{ index: 0, delta, finish_reason }], ...(finish_reason ? { usage: { prompt_tokens: 20, completion_tokens: 8, total_tokens: 28 } } : {}) })}\n\n`;
80
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
81
+ res.end(`${chunk({ role: 'assistant', content: 'Bigint support passed the pinned package tests and declared regression check; handoff recorded.' })}${chunk({}, 'stop')}data: [DONE]\n\n`);
82
+ },
83
+ });
84
+
85
+ try {
86
+ sourceDirectory = join(await mkdtemp(join(tmpdir(), 'ctg-public-repo-source-parent-')), 'checkout');
87
+ await mkdir(sourceDirectory);
88
+ execFileSync('git', ['clone', '--quiet', '--no-checkout', repository, sourceDirectory], { encoding: 'utf8', env: childEnv });
89
+ const git = (...args) => execFileSync('git', args, { cwd: sourceDirectory, encoding: 'utf8', env: childEnv }).trim();
90
+ execFileSync('git', ['checkout', '--quiet', '--detach', commit], { cwd: sourceDirectory, stdio: 'ignore', env: childEnv });
91
+ const sourceCommit = git('rev-parse', 'HEAD');
92
+ assert.equal(sourceCommit, commit, 'fixture must match the pinned upstream source commit');
93
+ assert.equal(git('status', '--porcelain=v1', '--untracked-files=no'), '', 'pinned checkout must have no tracked modifications');
94
+ assert.equal(JSON.parse(await readFile(join(sourceDirectory, 'package.json'), 'utf8')).license, license);
95
+ for (const file of ['index.js', 'package.json', 'LICENSE', 'test.js']) {
96
+ const blob = git('rev-parse', `${commit}:${file}`);
97
+ const copiedHash = execFileSync('git', ['hash-object', join(sourceDirectory, file)], { encoding: 'utf8', env: childEnv }).trim();
98
+ assert.equal(copiedHash, blob, `source file ${file} must match its exact pinned Git blob`);
99
+ }
100
+ // Acquisition is intentionally outside the offline native host/provider run.
101
+ // This historical package has no lockfile; install its declared dev dependencies
102
+ // only, in the fresh pinned clone, without package lifecycle scripts.
103
+ const dependencyInstall = spawnSync('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: sourceDirectory, encoding: 'utf8', env: childEnv });
104
+ assert.equal(dependencyInstall.status, 0, `Pinned checkout dev-dependency install failed: ${dependencyInstall.stdout}\n${dependencyInstall.stderr}`);
105
+ await mkdir(join(host.cwd, 'test'), { recursive: true });
106
+ await cp(join(sourceDirectory, 'node_modules'), join(host.cwd, 'node_modules'), { recursive: true });
107
+ await Promise.all(['index.js', 'package.json', 'LICENSE'].map(file => copyFile(join(sourceDirectory, file), join(host.cwd, file))));
108
+ await copyFile(join(sourceDirectory, 'test.js'), join(host.cwd, 'test.js'));
109
+ await writeFile(join(host.cwd, 'ctg-bigint.test.cjs'), testFile);
110
+ const gitInit = (...args) => execFileSync('git', args, { cwd: host.cwd, stdio: 'ignore', env: childEnv });
111
+ gitInit('init', '--initial-branch=main');
112
+ gitInit('add', 'index.js', 'package.json', 'LICENSE', 'test.js', 'ctg-bigint.test.cjs');
113
+ gitInit('-c', 'user.name=CTG public-repo fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-m', 'Pinned public repository baseline');
114
+ const runNpmTests = () => spawnSync('npm', ['test', '--', '--recursive', 'test.js'], { cwd: host.cwd, encoding: 'utf8', env: childEnv });
115
+ const runBigintTest = () => spawnSync(process.execPath, ['--test', 'ctg-bigint.test.cjs'], { cwd: host.cwd, encoding: 'utf8', env: childEnv });
116
+ const baselinePackage = runNpmTests();
117
+ assert.equal(baselinePackage.status, 0, `pinned package baseline suite must pass independently: ${baselinePackage.stdout}${baselinePackage.stderr}`);
118
+ assert.match(baselinePackage.stdout, /111 passing/i, 'baseline package suite must run all 111 tests');
119
+ const baselineBigint = runBigintTest();
120
+ assert.notEqual(baselineBigint.status, 0, `regression contract must fail independently on baseline: ${baselineBigint.stdout}${baselineBigint.stderr}`);
121
+ const pinnedTestBytes = await readFile(join(host.cwd, 'test.js'));
122
+ const pinnedIndexBytes = await readFile(join(host.cwd, 'index.js'));
123
+ try {
124
+ await writeFile(join(host.cwd, 'index.js'), pinnedIndexBytes.toString().replace(" if (typeof num === 'number') {", " if (typeof num === 'bigint') {\n return true;\n }\n if (typeof num === 'number') {"));
125
+ await writeFile(join(host.cwd, 'test.js'), `${pinnedTestBytes}\nit('controlled package-suite failure', function () { assert.equal(true, false); });\n`);
126
+ const negativePackage = runNpmTests();
127
+ const negativeBigint = runBigintTest();
128
+ assert.notEqual(negativePackage.status, 0, `controlled package failure was not detected: ${negativePackage.stdout}${negativePackage.stderr}`);
129
+ assert.equal(negativeBigint.status, 0, `independent bigint check should remain green: ${negativeBigint.stdout}${negativeBigint.stderr}`);
130
+ assert.equal(negativePackage.status === 0 && negativeBigint.status === 0, false, 'green bigint test must not mask failing package suite');
131
+ } finally {
132
+ await writeFile(join(host.cwd, 'index.js'), pinnedIndexBytes);
133
+ await writeFile(join(host.cwd, 'test.js'), pinnedTestBytes);
134
+ }
135
+ await host.active();
136
+ const result = await runNativeEvaluation({
137
+ client: host.client, directory: host.cwd,
138
+ metadata: { 'carthagent.options': { maxTools: 50, maxSeconds: 180, maxRepairs: 12 } },
139
+ prompt: `Complete a small feature task on this real pinned public package checkout, using the registered CTG production delivery tools. Repo=${repository} commit=${commit} license=${license}. First inspect and author a valid delivery plan with distinct checks npm test -- --recursive test.js and ${process.execPath} --test ctg-bigint.test.cjs. Run each separately: establish package suite green and bigint regression red before edit, then make the native edit and require each independently green. Run delivery_check id=all, record progress, inspect and record review coverage/probes honestly, and finish verified. No network access, upstream write, push, or public issue. Do not claim independent review.`,
140
+ provider: host.model.providerID, model: host.model.id, timeoutSeconds: 180, maxTurns: 24, maxCost: 1,
141
+ });
142
+ assert.equal(result.runtime, 'opencode-2');
143
+ assert.equal(result.compatibilityVersion, '2.0.32');
144
+ assert.equal(result.exitCode, 0, JSON.stringify({ result, lastRequests: host.requests.slice(-5).map(request => (request.messages ?? []).slice(-4)) }));
145
+ assert.equal(result.outcome, 'succeeded');
146
+ assert.ok(result.tools.edit >= 1, JSON.stringify(result));
147
+ assert.ok(result.tools.shell >= 2, JSON.stringify(result));
148
+ const finalPackage = runNpmTests();
149
+ assert.equal(finalPackage.status, 0, `package suite independent post-edit run failed: ${finalPackage.stdout}${finalPackage.stderr}`);
150
+ assert.match(finalPackage.stdout, /111 passing/i);
151
+ const finalBigint = runBigintTest();
152
+ assert.equal(finalBigint.status, 0, finalBigint.stdout + finalBigint.stderr);
153
+ assert.match(await readFile(join(host.cwd, 'index.js'), 'utf8'), /typeof num === 'bigint'/);
154
+ const events = [];
155
+ const callNames = new Map();
156
+ const seenCalls = new Set();
157
+ const seenResults = new Set();
158
+ for (const request of host.requests) for (const message of request.messages ?? []) {
159
+ for (const call of message.tool_calls ?? []) {
160
+ if (seenCalls.has(call.id)) continue;
161
+ seenCalls.add(call.id);
162
+ callNames.set(call.id, call.function?.name);
163
+ let args = {};
164
+ try { args = JSON.parse(call.function?.arguments ?? '{}'); } catch {}
165
+ events.push({ kind: 'call', id: call.id, name: call.function?.name, args });
166
+ }
167
+ if (message.role === 'tool' && !seenResults.has(message.tool_call_id)) {
168
+ seenResults.add(message.tool_call_id);
169
+ events.push({ kind: 'result', id: message.tool_call_id, name: callNames.get(message.tool_call_id), output: String(message.content) });
170
+ }
171
+ }
172
+ const shellResults = events.filter(event => event.kind === 'result' && event.name === 'shell');
173
+ assert.equal(shellResults.length, 4, 'native session must separately run package and bigint checks before and after edit');
174
+ assertNativeShellResult(shellResults[0].output, { status: 0, passingTests: 111 });
175
+ assertNativeShellResult(shellResults[1].output, { status: 1 });
176
+ assert.match(shellResults[1].output, /\b(?:fail|not ok)\s+1\b/i, 'bigint regression fails before edit');
177
+ assertNativeShellResult(shellResults[2].output, { status: 0, passingTests: 111 });
178
+ assertNativeShellResult(shellResults[3].output, { status: 0 });
179
+ assert.match(shellResults[3].output, /pass\s+1\b/i, 'bigint regression passes after edit');
180
+ const plan = events.find(event => event.kind === 'result' && event.name === 'execute' && /plan\.d2|Contract accepted/i.test(event.output));
181
+ assert.ok(plan, 'native production delivery plan must have a correlated tool result');
182
+ assert.ok(events.some(event => event.kind === 'result' && event.name === 'edit'), 'native edit must have a correlated result');
183
+ const recordCall = events.find(event => event.kind === 'call' && event.name === 'execute' && /delivery_review/.test(String(event.args.code ?? '')) && /record/.test(String(event.args.code ?? '')));
184
+ assert.ok(recordCall, 'native session must call delivery_review record');
185
+ const recordResult = events.find(event => event.kind === 'result' && event.id === recordCall.id);
186
+ assert.ok(recordResult, 'delivery_review record must have its own correlated result');
187
+ const recordReceipt = JSON.parse(recordResult.output);
188
+ assert.equal(recordReceipt.coverage?.[0]?.status, 'demonstrated', 'delivery_review record itself must retain coverage');
189
+ const finishCall = events.find(event => event.kind === 'call' && event.name === 'execute' && /delivery_finish/.test(String(event.args.code ?? '')));
190
+ assert.ok(finishCall, 'native session must invoke delivery_finish');
191
+ const finishResult = events.find(event => event.kind === 'result' && event.id === finishCall.id);
192
+ assert.ok(finishResult, 'delivery_finish must have its own correlated result');
193
+ let finishPayload;
194
+ try { finishPayload = JSON.parse(finishResult.output); } catch { finishPayload = {}; }
195
+ assert.equal(finishPayload.status, 'verified', 'delivery_finish result itself must be verified');
196
+ const reportPath = finishPayload.report;
197
+ assert.ok(reportPath, 'verified finish output must identify its persisted delivery report');
198
+ const report = JSON.parse(await readFile(reportPath, 'utf8'));
199
+ assert.equal(report.handoff?.status, 'verified', 'persisted delivery report handoff must retain verified status');
200
+ let retainedReportPath = null;
201
+ if (proofDirectory) {
202
+ await mkdir(proofDirectory, { recursive: true });
203
+ retainedReportPath = join(proofDirectory, 'delivery-report.json');
204
+ await copyFile(reportPath, retainedReportPath);
205
+ }
206
+ const baselineAt = events.indexOf(shellResults[0]);
207
+ const editAt = events.findIndex(event => event.kind === 'result' && event.name === 'edit');
208
+ const redAt = events.indexOf(shellResults[1]);
209
+ const greenAt = events.indexOf(shellResults[2]);
210
+ assert.ok(baselineAt < redAt && redAt < editAt && editAt < greenAt, 'correlated native chronology is green baseline suite, red regression, edit, then green package suite');
211
+ const correlatedNativeEvidence = events.filter(event => event.kind === 'result' && (event.name === 'shell' || event.name === 'edit' || (event.name === 'execute' && /delivery_(?:plan|check|progress|review|finish)/.test(String(events.find(call => call.kind === 'call' && call.id === event.id)?.args.code ?? '')))));
212
+ let harnessSourceIdentity = { kind: 'installed-package', packageVersion };
213
+ try {
214
+ const sourceCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', env: childEnv }).trim();
215
+ if (/^[a-f0-9]{40,64}$/.test(sourceCommit)) harnessSourceIdentity = { kind: 'git-checkout', commit: sourceCommit };
216
+ } catch { /* Installed package archives do not necessarily include .git. */ }
217
+ console.log(JSON.stringify({ harnessSourceIdentity, harnessSourceSha256: createHash('sha256').update(await readFile(new URL(import.meta.url))).digest('hex'), shellResultValidatorSha256: createHash('sha256').update(await readFile(new URL('./native-public-repo-shell-result.mjs', import.meta.url))).digest('hex'), repository, commit, license, sourceBlobs: Object.fromEntries(['index.js', 'package.json', 'LICENSE', 'test.js'].map(file => [file, git('rev-parse', `${commit}:${file}`)])), baseline: '111 package tests passed; declared bigint contract failed', nativeEdit: 'bigint primitive branch added to index.js', postEdit: 'package suite plus bigint regression passed', dependencyInstall: 'npm install --ignore-scripts --no-audit --no-fund from fresh pinned clone; repository has no lockfile', nativeDelivery: 'plan/check/progress/review-record/finish exercised', verifiedFinish: { correlatedCallId: finishCall.id, result: finishPayload, retainedReportPath, persistedHandoffStatus: report.handoff?.status }, outcome: result.outcome, productiveTurns: result.productiveTurns, tools: result.tools, cost: result.cost, runtimeVersion: result.runtimeVersion, runtimeIdentity: host.runtimeIdentity, correlatedNativeEvidence, upstreamWrites: false }));
218
+ } catch (error) {
219
+ console.error(JSON.stringify({ providerErrors: host.providerErrors.map(item => item.message), host: host.output }, null, 2));
220
+ throw error;
221
+ } finally {
222
+ await host.close();
223
+ if (sourceDirectory) await rm(join(sourceDirectory, '..'), { recursive: true, force: true });
224
+ }
@@ -0,0 +1,106 @@
1
+ /** Real pinned host regression: expired OAuth must not resolve its own refresh.
2
+ * Only disposable fixture credentials and loopback traffic are used. */
3
+ import assert from 'node:assert/strict';
4
+ import { createServer } from 'node:http';
5
+ import { once } from 'node:events';
6
+ import { nativeFixture } from './native-fixture.mjs';
7
+ import { assertPluginCondition } from './native-paired-guards.mjs';
8
+
9
+ let rotations = 0;
10
+ let discoveries = 0;
11
+ let switchAccount;
12
+ const cloud = createServer(async (req, res) => {
13
+ let raw = '';
14
+ for await (const chunk of req) raw += chunk;
15
+ if (req.url === '/v1/token/refresh') {
16
+ assert.equal(JSON.parse(raw).refreshToken, 'fixture-expired-refresh');
17
+ rotations++;
18
+ await switchAccount?.();
19
+ res.writeHead(200, { 'content-type': 'application/json' });
20
+ res.end(JSON.stringify({ accessToken: 'fixture-rotated-access', refreshToken: 'fixture-rotated-refresh', expiresIn: 3600 }));
21
+ return;
22
+ }
23
+ if (req.url === '/v1/models' && ['Bearer fixture-rotated-access', 'Bearer fixture-account-b'].includes(req.headers.authorization)) {
24
+ discoveries++;
25
+ res.writeHead(200, { 'content-type': 'application/json' });
26
+ res.end(JSON.stringify({ data: [{ id: 'refresh-reentry-fixture' }] }));
27
+ return;
28
+ }
29
+ res.writeHead(404); res.end('{}');
30
+ });
31
+ let fixture;
32
+ try {
33
+ cloud.listen(0, '127.0.0.1');
34
+ await once(cloud, 'listening');
35
+ // Installed-product fixture, like test-native-controls: source-snapshot
36
+ // verification requires the checkout lockfile and is not a package contract.
37
+ // Verify the real installed inventory and health explicitly at each start.
38
+ fixture = await nativeFixture({ plugin: process.argv[2],
39
+ env: { CARTHAGENT_CLOUD_URL: `http://127.0.0.1:${cloud.address().port}`,
40
+ EXP_GATEWAY_URL: `http://127.0.0.1:${cloud.address().port}/v1` } });
41
+ // Use the same location for activation and assertions, as the Ship sibling.
42
+ await fixture.client.session.create({ location: fixture.location, model: fixture.model,
43
+ title: 'native plugin activation probe' }, fixture.request());
44
+ await fixture.active();
45
+ const location = fixture.location;
46
+ const verifyPlugin = async () => {
47
+ const inventory = (await fixture.client.plugin.list({ location }, fixture.request())).data;
48
+ assertPluginCondition(inventory, true);
49
+ assert.equal(inventory.filter(plugin => plugin.id === 'carthagent').length, 1);
50
+ assert.ok(!inventory.some(plugin => ['failed', 'error'].includes(plugin.state.status)));
51
+ const health = (await fixture.client.rpc.call({ rpcID: 'carthagent', method: 'health', input: {}, location }, fixture.request())).output;
52
+ assert.equal(health.hostVersion, fixture.runtimeIdentity.runtimeVersion);
53
+ assert.equal(health.protocol, 1);
54
+ assert.equal(health.directory, location.directory);
55
+ };
56
+ await verifyPlugin();
57
+ const otherValue = { type: 'oauth', methodID: 'carthagent-device', access: 'fixture-account-b',
58
+ refresh: 'fixture-refresh-b', expires: Date.now() + 3600000 };
59
+ const other = await fixture.client.credential.create({ integrationID: 'experiential-labs',
60
+ value: otherValue, activate: true }, fixture.request());
61
+ switchAccount = () => fixture.client.credential.activate({ credentialID: other.id }, fixture.request());
62
+ const created = await fixture.client.credential.create({ integrationID: 'experiential-labs',
63
+ value: { type: 'oauth', methodID: 'carthagent-device', access: 'fixture-expired-access',
64
+ refresh: 'fixture-expired-refresh', expires: 1 }, activate: true }, fixture.request());
65
+ await Promise.all(Array.from({ length: 8 }, () => fixture.client.model.list({ location }, fixture.request(8000))));
66
+ const deadline = Date.now() + 8000;
67
+ while (true) {
68
+ const stored = (await fixture.client.credential.list(fixture.request(8000))).find(entry => entry.id === created.id);
69
+ if (stored?.value.refresh === 'fixture-rotated-refresh') break;
70
+ assert.ok(Date.now() < deadline, `Expired OAuth refresh must persist without resolve reentry (rotations=${rotations}, discoveries=${discoveries})`);
71
+ await new Promise(resolve => setTimeout(resolve, 40));
72
+ }
73
+ assert.equal(rotations, 1);
74
+ const accounts = await fixture.client.credential.list(fixture.request());
75
+ assert.deepEqual(accounts.find(entry => entry.id === other.id).value, otherValue, 'Switching to B must not copy B into A or mutate B');
76
+ assert.equal(accounts.find(entry => entry.id === other.id).active, true);
77
+ // stop() awaits process exit: this is the completion barrier for every
78
+ // discovery task started under A. Test B's catalog after restart rather
79
+ // than racing independent A/B background provider publications. No response
80
+ // delay can establish host-side publication order.
81
+ await fixture.stop();
82
+ const discoveriesBeforeRestart = discoveries;
83
+ await fixture.start();
84
+ await fixture.active();
85
+ await verifyPlugin();
86
+ const persisted = (await fixture.client.credential.list(fixture.request())).find(entry => entry.id === created.id);
87
+ assert.equal(persisted.value.refresh, 'fixture-rotated-refresh');
88
+ const restartedAccounts = await fixture.client.credential.list(fixture.request());
89
+ assert.equal(restartedAccounts.find(entry => entry.id === other.id).active, true);
90
+ const modelDeadline = Date.now() + 8000;
91
+ while (true) {
92
+ const models = (await fixture.client.model.list({ location }, fixture.request(8000))).data;
93
+ if (models.some(model => model.id === 'refresh-reentry-fixture' && model.providerID === 'experiential-labs')) break;
94
+ assert.ok(Date.now() < modelDeadline, `Active B must publish the exact Ship model after restart: ${JSON.stringify(models.map(model => ({ id: model.id, providerID: model.providerID })))}`);
95
+ await new Promise(resolve => setTimeout(resolve, 40));
96
+ }
97
+ assert.ok(discoveries > discoveriesBeforeRestart, 'Restart must discover B models rather than reuse an A catalog');
98
+ assert.equal(rotations, 1, 'restart must reuse the host-persisted rotation');
99
+ console.log(JSON.stringify({ passed: true, runtime: fixture.runtimeIdentity.runtimeVersion,
100
+ binarySha256: fixture.runtimeIdentity.binarySha256, rotations, discoveries,
101
+ concurrentRequests: 8, persistedAcrossRestart: true, accountSwitchIsolated: true }));
102
+ } finally {
103
+ await fixture?.close();
104
+ cloud.closeAllConnections();
105
+ await new Promise(resolve => cloud.close(resolve));
106
+ }
@@ -9,26 +9,16 @@ import { createReport } from '../lib/reports.mjs';
9
9
  import { selectReport } from '../lib/workspace.mjs';
10
10
  import { nativeFixture } from './native-fixture.mjs';
11
11
 
12
- const plugin = process.argv[2] ?? join(import.meta.dirname, 'native-report-plugin');
13
- const host = await nativeFixture({ plugin });
12
+ const host = await nativeFixture({ plugin: process.argv[2] });
13
+ const identity = host.runtimeIdentity;
14
14
  try {
15
15
  execFileSync('git', ['init', '-q'], { cwd: host.cwd });
16
16
  await writeFile(join(host.cwd, 'source.txt'), 'original\n');
17
- const activationDeadline = Date.now() + 15_000;
18
- while (true) {
19
- const inventory = await host.client.plugin.list({ location: host.location }, host.request());
20
- const pluginState = inventory.data.find(item => item.id === 'carthagent-report-fixture')?.state.status;
21
- if (pluginState === 'active') break;
22
- if (Date.now() >= activationDeadline) throw Error(`Report fixture plugin did not activate: ${JSON.stringify(inventory.data)}\n${host.output}`);
23
- await new Promise(done => setTimeout(done, 50));
24
- }
17
+ await host.active();
25
18
  const session = await host.client.session.create({ location: host.location, model: host.model, title: 'Report projection' }, host.request());
26
19
  const read = async () => (await host.client.rpc.call({
27
20
  rpcID: 'carthagent.workspace', method: 'delivery', input: { sessionID: session.id }, location: host.location,
28
21
  }, host.request())).output;
29
- const probe = method => host.client.rpc.call({
30
- rpcID: 'carthagent.report-schema-probe', method, input: {}, location: host.location,
31
- }, host.request());
32
22
  const check = { id: 'unit', kind: 'test', argv: ['node', '--test'], timeoutSeconds: 20 };
33
23
  const plan = {
34
24
  goal: 'Show authoritative report data', verification: 'required', artifacts: ['.'], outputs: [],
@@ -61,12 +51,6 @@ try {
61
51
  assert.equal(current.architecture.nodes[0].title, 'Project report state');
62
52
  assert.ok(!JSON.stringify(current).includes('PRIVATE_REPORT_LOG_SENTINEL'));
63
53
  assert.ok(!JSON.stringify(current).includes(host.cwd));
64
- await assert.rejects(probe('incompleteVerified'), error => error.type === 'rpc.invalid_output');
65
- const invalidWithFields = await probe('invalidWithFullFields');
66
- assert.deepEqual(invalidWithFields.output, { status: 'invalid' });
67
- assert.deepEqual((await probe('validUnavailable')).output, { status: 'unavailable' });
68
- assert.deepEqual((await probe('validInvalid')).output, { status: 'invalid' });
69
- assert.equal((await probe('validActive')).output.status, 'active');
70
54
 
71
55
  await writeFile(join(host.cwd, 'source.txt'), 'modified\n');
72
56
  const stale = await read();
@@ -82,7 +66,30 @@ try {
82
66
  selectReport(host.cwd, report.path);
83
67
  await writeFile(report.path, '{corrupt');
84
68
  assert.deepEqual(await read(), { status: 'invalid' });
85
- console.log(JSON.stringify({ host: '2.0.32', deliveryOutputSchema: 'passed', freshness: 'passed', verifiedFailClosed: 'passed', ownership: 'passed' }));
69
+ console.log(JSON.stringify({ host: identity.runtimeVersion, compatibilityVersion: identity.compatibilityVersion,
70
+ ...(identity.binarySha256 ? { binarySha256: identity.binarySha256 } : {}),
71
+ deliveryOutputSchema: 'passed', freshness: 'passed', verifiedFailClosed: 'passed', ownership: 'passed' }));
86
72
  } finally {
87
73
  await host.close();
88
74
  }
75
+
76
+ // Deliberately malformed outputs require a test-only handler. Exercise that
77
+ // handler separately so production ownership/report checks use the shipped plugin.
78
+ const schemaHost = await nativeFixture({ plugin: join(import.meta.dirname, 'native-report-plugin') });
79
+ const schemaIdentity = schemaHost.runtimeIdentity;
80
+ try {
81
+ await schemaHost.active('carthagent-report-fixture');
82
+ const probe = method => schemaHost.client.rpc.call({
83
+ rpcID: 'carthagent.report-schema-probe', method, input: {}, location: schemaHost.location,
84
+ }, schemaHost.request());
85
+ await assert.rejects(probe('incompleteVerified'), error => error.type === 'rpc.invalid_output');
86
+ assert.deepEqual((await probe('invalidWithFullFields')).output, { status: 'invalid' });
87
+ assert.deepEqual((await probe('validUnavailable')).output, { status: 'unavailable' });
88
+ assert.deepEqual((await probe('validInvalid')).output, { status: 'invalid' });
89
+ assert.equal((await probe('validActive')).output.status, 'active');
90
+ console.log(JSON.stringify({ host: schemaIdentity.runtimeVersion, compatibilityVersion: schemaIdentity.compatibilityVersion,
91
+ ...(schemaIdentity.binarySha256 ? { binarySha256: schemaIdentity.binarySha256 } : {}),
92
+ reportSchemaVariants: 'passed', incompleteVerified: 'rejected' }));
93
+ } finally {
94
+ await schemaHost.close();
95
+ }