carthagent 1.0.7 → 1.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/beta-rewards-e2e-plan.md +48 -0
  4. package/docs/beta-rewards-e2e-results.md +84 -0
  5. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  6. package/docs/glyph-review/test-isolation-v1.md +26 -0
  7. package/docs/glyph-review/typecheck-base.log +1 -0
  8. package/docs/glyph-review/typecheck-v4.log +1 -0
  9. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  10. package/docs/help-dispatch-review.md +69 -0
  11. package/docs/launch-readiness-live-results.md +65 -0
  12. package/docs/legacy-cli.md +150 -0
  13. package/docs/live-theme-plugin-api.md +18 -0
  14. package/docs/native-cli.md +102 -0
  15. package/docs/native-compatibility.md +16 -0
  16. package/docs/native-daily-quests.md +132 -0
  17. package/docs/native-glyph-pty.md +59 -0
  18. package/docs/native-manual-testing.md +336 -0
  19. package/docs/native-slash-acceptance-handoff.md +49 -0
  20. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  21. package/docs/native-terminal-performance.md +310 -0
  22. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  23. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  24. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  35. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  36. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  37. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  38. package/docs/native-terminal-semantics.md +89 -0
  39. package/docs/native-upgrade.md +175 -0
  40. package/docs/opencode-composed-source-review.md +89 -0
  41. package/docs/opencode-migration.md +225 -9
  42. package/docs/opencode-owned-runtime.md +241 -0
  43. package/docs/opencode-runtime.md +12 -0
  44. package/docs/opencode-theme-provider-proof/README.md +55 -0
  45. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  46. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  47. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  48. package/docs/owned-host-controls-proof.md +39 -0
  49. package/docs/owned-runtime-release-manifest.md +42 -0
  50. package/docs/owned-visual-clear-proof.md +40 -0
  51. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  52. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  53. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  54. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  61. package/docs/release-notes-beta.1.md +60 -0
  62. package/docs/release-notes-native-draft.md +73 -0
  63. package/docs/release-notes-preview.10.md +121 -0
  64. package/docs/release-notes-preview.11.md +76 -0
  65. package/docs/release-notes-preview.12.md +55 -0
  66. package/docs/release-notes-preview.13.md +68 -0
  67. package/docs/release-notes-preview.9.md +130 -0
  68. package/docs/source-preview.md +120 -0
  69. package/evaluate.mjs +28 -1
  70. package/lib/budget.mjs +1 -1
  71. package/lib/codegraph.mjs +1 -1
  72. package/lib/delivery-review.mjs +19 -6
  73. package/lib/delivery.mjs +3 -2
  74. package/lib/environment.mjs +8 -0
  75. package/lib/guidance.mjs +1 -1
  76. package/lib/opencode/cli-runtime.mjs +1171 -0
  77. package/lib/opencode/cli.mjs +252 -0
  78. package/lib/opencode/credentials.mjs +63 -8
  79. package/lib/opencode/demo.mjs +53 -0
  80. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  81. package/lib/opencode/evaluation.mjs +32 -12
  82. package/lib/opencode/headless.mjs +22 -5
  83. package/lib/opencode/provider-policy.mjs +23 -0
  84. package/lib/opencode/review.mjs +37 -16
  85. package/lib/opencode/runtime-artifact.mjs +335 -0
  86. package/lib/opencode/runtime-github-transport.mjs +121 -0
  87. package/lib/opencode/runtime-release.json +36 -0
  88. package/lib/opencode/runtime.mjs +299 -20
  89. package/lib/opencode/service.mjs +77 -35
  90. package/lib/opencode/session-options.mjs +6 -2
  91. package/lib/opencode/update.mjs +222 -0
  92. package/lib/opencode/workspace.mjs +1 -1
  93. package/lib/planning-advice.mjs +2 -2
  94. package/lib/providers/experiential.mjs +2 -2
  95. package/lib/tui/framing.mjs +1 -80
  96. package/lib/update.mjs +10 -1
  97. package/package.json +33 -7
  98. package/plugins/carthagent/README.md +23 -0
  99. package/plugins/carthagent/src/bootstrap.ts +1 -1
  100. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  101. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  102. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  103. package/plugins/carthagent/src/index.ts +71 -24
  104. package/plugins/carthagent/src/rpc.ts +51 -6
  105. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  106. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  107. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  108. package/plugins/carthagent/src/server/delivery.ts +227 -112
  109. package/plugins/carthagent/src/server/framing.mjs +80 -0
  110. package/plugins/carthagent/src/server/policy.ts +1 -1
  111. package/plugins/carthagent/src/server/providers.ts +59 -31
  112. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  113. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  114. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  115. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  116. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  117. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  118. package/plugins/carthagent/src/testing.ts +5 -0
  119. package/plugins/carthagent/src/tui.tsx +496 -0
  120. package/plugins/carthagent/src/ui/client.ts +127 -0
  121. package/plugins/carthagent/src/ui/commands.ts +70 -0
  122. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  123. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  124. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  125. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  126. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  127. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  128. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  129. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  130. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  131. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  132. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  133. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  134. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  135. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  136. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  137. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  138. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  139. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  140. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  141. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  142. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  143. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  144. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  145. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  146. package/plugins/carthagent/tests/commands.test.ts +107 -0
  147. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  148. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  149. package/plugins/carthagent/themes/carthage.json +67 -0
  150. package/plugins/carthagent/tui.ts +26 -1
  151. package/script/ctg/build-owned-runtime.sh +166 -0
  152. package/tests/budget-engine.test.mjs +4 -3
  153. package/tests/cli.test.mjs +52 -96
  154. package/tests/daily-evidence.test.mjs +62 -0
  155. package/tests/delivery-review.test.mjs +57 -2
  156. package/tests/delivery.test.mjs +22 -1
  157. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  158. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  159. package/tests/installed-runtime-contract.test.mjs +43 -0
  160. package/tests/native-cli-host-smoke.mjs +457 -0
  161. package/tests/native-daily-editor-focus.test.mjs +213 -0
  162. package/tests/native-daily-editor.test.mjs +43 -0
  163. package/tests/native-daily-quests-layout.test.mjs +55 -0
  164. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  165. package/tests/native-daily-quests-ui.test.mjs +778 -0
  166. package/tests/native-daily-source.test.mjs +525 -0
  167. package/tests/native-demo-ui.test.mjs +48 -0
  168. package/tests/native-evaluation-admission.test.mjs +24 -0
  169. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  170. package/tests/native-fixture-runtime.test.mjs +111 -0
  171. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  172. package/tests/native-glyph-pty.test.mjs +107 -0
  173. package/tests/native-manual-projects.test.mjs +170 -0
  174. package/tests/native-model-command.test.mjs +67 -0
  175. package/tests/native-owned-runtime-env.test.mjs +140 -0
  176. package/tests/native-paired-guards.test.mjs +464 -0
  177. package/tests/native-provider-policy.test.mjs +31 -0
  178. package/tests/native-pty-invocation.test.mjs +114 -0
  179. package/tests/native-pty-runtime.test.mjs +45 -0
  180. package/tests/native-resume-session.test.mjs +46 -0
  181. package/tests/native-review-consumer.test.mjs +286 -0
  182. package/tests/native-runtime-identity.test.mjs +214 -0
  183. package/tests/native-select-mouse.test.mjs +23 -0
  184. package/tests/native-ship-guidance.test.mjs +16 -0
  185. package/tests/native-ship-parity.test.mjs +526 -0
  186. package/tests/native-ship-plan.test.mjs +85 -0
  187. package/tests/native-ship-rpc.test.mjs +71 -0
  188. package/tests/native-ui-commands.test.mjs +6 -0
  189. package/tests/native-voyages-layout.test.mjs +28 -0
  190. package/tests/native-voyages-navigation.test.mjs +169 -0
  191. package/tests/native-voyages-rpc.test.mjs +397 -0
  192. package/tests/native-voyages-ui.test.mjs +307 -0
  193. package/tests/opencode-atomic-write.test.mjs +55 -0
  194. package/tests/opencode-cli.test.mjs +1312 -0
  195. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  196. package/tests/opencode-delivery.test.mjs +16 -0
  197. package/tests/opencode-providers.test.mjs +245 -18
  198. package/tests/opencode-runtime.test.mjs +597 -8
  199. package/tests/opencode-session-controls.test.mjs +15 -0
  200. package/tests/opencode-ship-rpc.test.mjs +71 -0
  201. package/tests/opencode-update.test.mjs +475 -0
  202. package/tests/opencode-workspace.test.mjs +51 -2
  203. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  204. package/tests/release-workflow.test.mjs +46 -0
  205. package/tests/runtime-artifact.test.mjs +269 -0
  206. package/tests/runtime-development-trust.test.mjs +231 -0
  207. package/tests/runtime-github-transport.test.mjs +132 -0
  208. package/tests/runtime-token-env.test.mjs +23 -0
  209. package/tests/server.test.mjs +3 -1
  210. package/tests/ship-telemetry.test.mjs +1 -1
  211. package/tests/source-preview.test.mjs +253 -0
  212. package/tests/test_native_terminal_probe.py +76 -0
  213. package/tests/test_native_terminal_semantics.py +232 -0
  214. package/tests/update.test.mjs +42 -5
  215. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  216. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  217. package/tests/workspace-live.test.mjs +127 -0
  218. package/tools/benchmark/diagnose-request.mjs +59 -36
  219. package/tools/benchmark/eval-common.mjs +3 -3
  220. package/tools/benchmark/evaluators.test.mjs +4 -4
  221. package/tools/benchmark/guard.test.mjs +35 -2
  222. package/tools/benchmark/guard.ts +20 -7
  223. package/tools/benchmark/planning-startup.test.mjs +30 -34
  224. package/tools/benchmark/runner.mjs +12 -7
  225. package/tools/benchmark/runner.test.mjs +23 -7
  226. package/tools/benchmark/upstream-error.test.mjs +16 -2
  227. package/tools/generate-owned-runtime-release.mjs +217 -0
  228. package/tools/installed-runtime-contract.mjs +16 -0
  229. package/tools/manual-runtime-manifest.mjs +39 -0
  230. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  231. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  232. package/tools/native-fixture.mjs +199 -40
  233. package/tools/native-glyph-helpers.py +127 -0
  234. package/tools/native-glyph-pty.py +144 -0
  235. package/tools/native-manual-projects.mjs +259 -0
  236. package/tools/native-manual.mjs +338 -0
  237. package/tools/native-model-picker-acceptance.py +306 -0
  238. package/tools/native-owned-runtime-env.sh +47 -0
  239. package/tools/native-paired-guards.mjs +282 -0
  240. package/tools/native-paired-manifest.mjs +72 -0
  241. package/tools/native-paired-orchestration.sh +51 -0
  242. package/tools/native-plugin-readiness.mjs +33 -0
  243. package/tools/native-plugin-readiness.test.mjs +53 -0
  244. package/tools/native-pty-invocation.mjs +26 -0
  245. package/tools/native-pty-runtime.mjs +83 -0
  246. package/tools/native-pty-smoke.py +147 -24
  247. package/tools/native-public-repo-shell-result.mjs +13 -0
  248. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  249. package/tools/native-runtime-identity.mjs +201 -0
  250. package/tools/native-slash-acceptance-env.py +38 -0
  251. package/tools/native-terminal-semantics.py +876 -0
  252. package/tools/owned-shell-observer/index.ts +16 -0
  253. package/tools/owned-shell-observer/package.json +1 -0
  254. package/tools/owned-visual-clear-env.py +48 -0
  255. package/tools/owned-visual-clear-model.py +21 -0
  256. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  257. package/tools/prepare-native-manual.mjs +122 -0
  258. package/tools/run-native-terminal-paired.sh +403 -0
  259. package/tools/runtime-token-env.mjs +8 -0
  260. package/tools/seed-native-switch-fixture.mjs +79 -0
  261. package/tools/test-native-controls.mjs +3 -1
  262. package/tools/test-native-daily-quests.mjs +65 -0
  263. package/tools/test-native-default-cli.mjs +106 -0
  264. package/tools/test-native-delivery-host.mjs +102 -20
  265. package/tools/test-native-evaluation.mjs +7 -3
  266. package/tools/test-native-glyph-pty.mjs +94 -0
  267. package/tools/test-native-host.mjs +87 -63
  268. package/tools/test-native-outcomes.mjs +3 -1
  269. package/tools/test-native-package.mjs +206 -38
  270. package/tools/test-native-performance.mjs +26 -0
  271. package/tools/test-native-product.mjs +90 -0
  272. package/tools/test-native-pty.mjs +34 -9
  273. package/tools/test-native-public-repo.mjs +224 -0
  274. package/tools/test-native-refresh-reentry.mjs +106 -0
  275. package/tools/test-native-report.mjs +27 -20
  276. package/tools/test-native-review.mjs +75 -0
  277. package/tools/test-native-runtime.mjs +120 -0
  278. package/tools/test-native-ship-models.mjs +176 -0
  279. package/tools/test-native-ship-walkthrough.mjs +96 -0
  280. package/tools/test-native-slash-commands.mjs +125 -0
  281. package/tools/test-native-slash-commands.py +242 -0
  282. package/tools/test-native-swe-host.mjs +224 -55
  283. package/tools/test-native-tasks.mjs +77 -0
  284. package/tools/test-native-terminal-performance.py +887 -0
  285. package/tools/test-native-terminal-readiness.sh +85 -0
  286. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  287. package/tools/test-native-visual-clear-installed.mjs +48 -0
  288. package/tools/test-owned-host-controls.mjs +238 -0
  289. package/tools/test-owned-opencode-runtime.mjs +35 -0
  290. package/tools/test-owned-visual-clear-env.py +120 -0
  291. package/tools/test-owned-visual-clear.py +373 -0
  292. package/tools/verify-draft-runtime-gate.mjs +138 -0
  293. package/tools/verify-owned-runtime-release.mjs +114 -0
  294. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Configure fixture-seeder identity selection for an explicitly selected owned
4
+ # runtime. The PTY client may still receive --binary directly; the Node fixture
5
+ # must resolve through the owned-root path so runtimeIdentity does not treat an
6
+ # ambient CTG_OPENCODE_BIN override as an upstream selection.
7
+ configure_native_owned_runtime() {
8
+ local binary="$1" owned_root="$2" release_manifest="$3"
9
+ if [[ ! -x "$binary" ]]; then
10
+ printf 'Owned native runtime is missing or not executable: %s\n' "$binary" >&2
11
+ return 2
12
+ fi
13
+ if [[ ! -f "$release_manifest" ]]; then
14
+ printf 'Trusted CTG runtime release record is required: %s\n' "$release_manifest" >&2
15
+ return 2
16
+ fi
17
+ local runtime_helper="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/native-runtime-identity.mjs"
18
+ local canonical_binary canonical_root canonical_manifest
19
+ canonical_binary="$(realpath "$binary")"
20
+ canonical_root="$(realpath "$owned_root")"
21
+ canonical_manifest="$(realpath "$release_manifest")"
22
+ if command -v cygpath >/dev/null 2>&1; then
23
+ runtime_helper="$(cygpath -w "$runtime_helper")"
24
+ canonical_binary="$(cygpath -w "$canonical_binary")"
25
+ canonical_root="$(cygpath -w "$canonical_root")"
26
+ canonical_manifest="$(cygpath -w "$canonical_manifest")"
27
+ fi
28
+ if ! node --input-type=module - "$runtime_helper" "$canonical_binary" "$canonical_root" "$canonical_manifest" >/dev/null <<'NODE'
29
+ import { readFile } from 'node:fs/promises';
30
+ import { pathToFileURL } from 'node:url';
31
+ const { loadNativeTestRuntime } = await import(pathToFileURL(process.argv[2]).href);
32
+ const identity = await loadNativeTestRuntime({
33
+ binary: process.argv[3],
34
+ ownedRoot: process.argv[4],
35
+ expectedManifest: JSON.parse(await readFile(process.argv[5], 'utf8')),
36
+ env: process.env,
37
+ });
38
+ process.stdout.write(`${JSON.stringify(identity)}\n`);
39
+ NODE
40
+ then
41
+ printf 'Owned native runtime failed trusted identity verification; refusing to launch fixture seeder.\n' >&2
42
+ return 2
43
+ fi
44
+ export CTG_OWNED_OPENCODE_RUNTIME="$canonical_root"
45
+ export CTG_TEST_RUNTIME_RELEASE_MANIFEST="$canonical_manifest"
46
+ unset CTG_OPENCODE_BIN
47
+ }
@@ -0,0 +1,282 @@
1
+ /** Fail-closed checks shared by paired native performance fixtures. */
2
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, readlinkSync, statSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import { createRequire } from 'node:module';
5
+ import path from 'node:path';
6
+ import { dirname, resolve, join, extname } from 'node:path';
7
+
8
+ export function assertCompletePluginSource(sourceRoot) {
9
+ const root = resolve(sourceRoot);
10
+ const required = ['package-lock.json', 'lib/ship-auth.mjs', 'plugins/carthagent/index.ts', 'plugins/carthagent/src/index.ts',
11
+ 'plugins/carthagent/src/rpc.ts', 'plugins/carthagent/package.json',
12
+ 'plugins/carthagent/src/server/session-controls.ts', 'plugins/carthagent/src/server/providers.ts',
13
+ 'plugins/carthagent/src/server/device-auth.ts', 'plugins/carthagent/src/server/workspace-rpc.ts',
14
+ 'plugins/carthagent/src/server/pull-request-rpc.ts', 'plugins/carthagent/src/server/archive-rpc.ts'];
15
+ const missing = required.filter(path => !existsSync(join(root, path)));
16
+ if (missing.length) throw new Error(`Incomplete native plugin source snapshot; missing ${missing.join(', ')}`);
17
+ assertPluginImportDependencies(join(root, 'plugins/carthagent/src/index.ts'));
18
+ assertWorkspacePluginResolution(root);
19
+ return { sourceRoot: root, requiredFiles: required };
20
+ }
21
+
22
+ export function assertWorkspacePluginResolution(sourceRoot) {
23
+ const root = resolve(sourceRoot);
24
+ const searchPaths = createRequire(join(root, 'plugins/carthagent/package.json')).resolve.paths('@opencode/plugin') ?? [];
25
+ const physicalPlugin = searchPaths.map(directory => join(directory, '@opencode/plugin')).find(directory => existsSync(directory));
26
+ const expectedPlugin = resolve(root, 'node_modules/@opencode/plugin');
27
+ if (!physicalPlugin || !existsSync(expectedPlugin) || realpathSync(physicalPlugin) !== realpathSync(expectedPlugin)) {
28
+ throw new Error(`Plugin workspace dependency identity mismatch: ${physicalPlugin} does not resolve to ${expectedPlugin}`);
29
+ }
30
+ return true;
31
+ }
32
+
33
+ export function hashDependencyPackages(nodeModulesRoot, sourcePackageJson, sourceRootOverride) {
34
+ const root = resolve(nodeModulesRoot);
35
+ const sourceRoot = resolve(sourceRootOverride ?? resolve(sourcePackageJson, '..'));
36
+ const packages = ['@opencode/cli', '@opencode/client', '@opencode/plugin'];
37
+ const sourceManifest = JSON.parse(readFileSync(sourcePackageJson, 'utf8'));
38
+ const pluginManifest = JSON.parse(readFileSync(join(sourceRoot, 'plugins/carthagent/package.json'), 'utf8'));
39
+ const lock = JSON.parse(readFileSync(join(sourceRoot, 'package-lock.json'), 'utf8'));
40
+ return Object.fromEntries(packages.map(name => {
41
+ const packageRoot = name === '@opencode/plugin' ? join(sourceRoot, 'node_modules/@opencode/plugin') : join(root, name);
42
+ const manifestPath = join(packageRoot, 'package.json');
43
+ if (!existsSync(manifestPath)) throw new Error(`Pinned dependency package is missing: ${manifestPath}`);
44
+ const bytes = readFileSync(manifestPath);
45
+ const manifest = JSON.parse(bytes.toString('utf8'));
46
+ const lockVersion = name === '@opencode/plugin'
47
+ ? lock.packages?.['node_modules/@opencode/plugin']?.version ?? lock.packages?.['plugins/carthagent']?.dependencies?.[name]
48
+ : lock.packages?.[`node_modules/${name}`]?.version;
49
+ const sourceRequirement = sourceManifest.dependencies?.[name] ?? sourceManifest.devDependencies?.[name] ??
50
+ pluginManifest.dependencies?.[name] ?? lock.packages?.['']?.dependencies?.[name] ?? lock.packages?.['']?.devDependencies?.[name];
51
+ if (sourceRequirement !== manifest.version) {
52
+ throw new Error(`Installed ${name}@${manifest.version} does not match source requirement ${sourceRequirement ?? '<missing>'}`);
53
+ }
54
+ if (lockVersion !== manifest.version) throw new Error(`Installed ${name}@${manifest.version} does not match selected lockfile ${lockVersion ?? '<missing>'}`);
55
+ const tree = createHash('sha256');
56
+ const visit = directory => {
57
+ for (const entry of readdirSync(directory).sort()) {
58
+ const path = join(directory, entry);
59
+ const relative = path.slice(packageRoot.length + 1);
60
+ const stat = lstatSync(path);
61
+ tree.update(`${relative}\0`);
62
+ if (stat.isSymbolicLink()) tree.update(`link:${readlinkSync(path)}\0`);
63
+ else if (stat.isDirectory()) { tree.update('dir\0'); visit(path); }
64
+ else if (stat.isFile()) tree.update(createHash('sha256').update(readFileSync(path)).digest());
65
+ }
66
+ };
67
+ visit(packageRoot);
68
+ return [name, { version: manifest.version, realpath: realpathSync(packageRoot),
69
+ packageJsonSha256: createHash('sha256').update(bytes).digest('hex'), treeSha256: tree.digest('hex') }];
70
+ }));
71
+ }
72
+
73
+ export function hashDependencyInstall(nodeModulesRoot) {
74
+ const root = resolve(nodeModulesRoot);
75
+ const digest = createHash('sha256');
76
+ const visitedDirectories = new Set();
77
+ const visitedFiles = new Set();
78
+ const visit = directory => {
79
+ const canonical = realpathSync(directory);
80
+ if (visitedDirectories.has(canonical)) return;
81
+ visitedDirectories.add(canonical);
82
+ digest.update(`directory:${canonical}\0`);
83
+ for (const entry of readdirSync(directory).sort()) {
84
+ const path = join(directory, entry);
85
+ const relative = path.slice(root.length + 1);
86
+ const stat = lstatSync(path);
87
+ digest.update(`${relative}\0`);
88
+ if (stat.isSymbolicLink()) {
89
+ const target = realpathSync(path);
90
+ digest.update(`link:${readlinkSync(path)}=>${target}\0`);
91
+ const targetStat = statSync(path);
92
+ if (targetStat.isDirectory()) visit(path);
93
+ else if (targetStat.isFile()) {
94
+ if (!visitedFiles.has(target)) {
95
+ visitedFiles.add(target);
96
+ digest.update(createHash('sha256').update(readFileSync(path)).digest());
97
+ }
98
+ }
99
+ }
100
+ else if (stat.isDirectory()) { digest.update('dir\0'); visit(path); }
101
+ else if (stat.isFile()) {
102
+ const canonicalFile = realpathSync(path);
103
+ if (!visitedFiles.has(canonicalFile)) {
104
+ visitedFiles.add(canonicalFile);
105
+ digest.update(createHash('sha256').update(readFileSync(path)).digest());
106
+ }
107
+ }
108
+ }
109
+ };
110
+ visit(root);
111
+ return { realpath: realpathSync(root), treeSha256: digest.digest('hex') };
112
+ }
113
+
114
+ export function assertDependencyPackageIdentity(expected, observed) {
115
+ if (!expected || !observed || expected.realpath !== observed.realpath || expected.treeSha256 !== observed.treeSha256) {
116
+ throw new Error('Dependency install identity mismatch');
117
+ }
118
+ return true;
119
+ }
120
+
121
+ export function assertPluginImportDependencies(pluginEntry) {
122
+ const sourceRoot = realpathSync(resolve(dirname(pluginEntry), '../../..'));
123
+ const canonicalEntry = realpathSync(resolve(pluginEntry));
124
+ if (!isPathWithinRoot(sourceRoot, canonicalEntry)) {
125
+ throw new Error(`Plugin source entry resolves outside immutable tree: ${canonicalEntry}`);
126
+ }
127
+ const scan = new Set();
128
+ const pending = [canonicalEntry];
129
+ const imports = [];
130
+ while (pending.length) {
131
+ const file = pending.pop();
132
+ if (scan.has(file)) continue;
133
+ scan.add(file);
134
+ const contents = readFileSync(file, 'utf8');
135
+ for (const specifier of [...contents.matchAll(/(?:import|export)\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/gm)].map(match => match[1])) {
136
+ imports.push(specifier);
137
+ if (!specifier.startsWith('.')) continue;
138
+ const importedPath = resolve(dirname(file), specifier);
139
+ const candidates = extname(importedPath)
140
+ ? [importedPath]
141
+ : [`${importedPath}.ts`, `${importedPath}.tsx`, `${importedPath}.mjs`, `${importedPath}.js`, `${importedPath}.json`, join(importedPath, 'index.ts'), join(importedPath, 'index.js')];
142
+ const importedFile = candidates.find(candidate => existsSync(candidate));
143
+ let canonicalImportedFile;
144
+ try {
145
+ if (importedFile) canonicalImportedFile = realpathSync(importedFile);
146
+ } catch {}
147
+ if (!canonicalImportedFile || !isPathWithinRoot(sourceRoot, canonicalImportedFile)) {
148
+ throw new Error(`Plugin source import is missing from immutable tree: ${specifier} (${importedFile})`);
149
+ }
150
+ pending.push(canonicalImportedFile);
151
+ }
152
+ }
153
+ const sourceImports = [...new Set(imports.filter(specifier => specifier.startsWith('.')))];
154
+ const workspaceImports = imports.filter(specifier => specifier === '@opencode/plugin' || specifier.startsWith('@opencode/plugin/'));
155
+ if (scan.size > 1 && !workspaceImports.includes('@opencode/plugin')) throw new Error('Plugin entry does not import the pinned @opencode/plugin runtime');
156
+ return { sourceImports, workspaceImports };
157
+ }
158
+
159
+ export function isPathWithinRoot(root, target, pathApi = path) {
160
+ const importedRelative = pathApi.relative(root, target);
161
+ return importedRelative === '' || (importedRelative !== '..' &&
162
+ !importedRelative.startsWith(`..${pathApi.sep}`) && !importedRelative.startsWith(pathApi.sep) &&
163
+ !/^[a-zA-Z]:[\\/]/.test(importedRelative));
164
+ }
165
+
166
+ export function assertPluginCondition(inventory, enabled) {
167
+ if (!Array.isArray(inventory)) throw new Error('Native plugin inventory is missing or malformed');
168
+ const carthagent = inventory.filter(plugin => plugin?.id === 'carthagent' ||
169
+ (typeof plugin?.id === 'string' && plugin.id.startsWith('carthagent.')));
170
+ if (enabled) {
171
+ const active = carthagent.find(plugin => plugin.id === 'carthagent' && plugin.state?.status === 'active');
172
+ if (!active) {
173
+ const details = carthagent.map(plugin => ({ id: plugin.id, status: plugin.state?.status, error: plugin.state?.error }));
174
+ throw new Error(`Expected native Carthagent plugin to be active; observed ${JSON.stringify(details)}`);
175
+ }
176
+ return { expected: 'active', observed: 'active', pluginId: 'carthagent' };
177
+ }
178
+ const enabledPlugins = inventory.filter(plugin => plugin?.id === 'carthagent' ||
179
+ (typeof plugin?.id === 'string' && plugin.id.startsWith('carthagent.')));
180
+ if (enabledPlugins.length) {
181
+ const details = enabledPlugins.map(plugin => ({ id: plugin.id, status: plugin.state?.status }));
182
+ throw new Error(`Baseline must have no Carthagent plugin; observed ${JSON.stringify(details)}`);
183
+ }
184
+ return { expected: 'absent', observed: 'absent', pluginId: 'carthagent' };
185
+ }
186
+
187
+ export function aggregateConditionExit(statuses, interrupted = false) {
188
+ const expected = ['baseline-1', 'ctg-1', 'ctg-2', 'baseline-2'];
189
+ if (interrupted || !statuses || expected.some(name => !Number.isInteger(statuses[name]) || statuses[name] !== 0)) return 1;
190
+ return 0;
191
+ }
192
+
193
+ export function assertSelectedSourceHead(selectedCommit, checkoutHead) {
194
+ if (typeof selectedCommit !== 'string' || !/^[a-f0-9]{40}$/.test(selectedCommit) ||
195
+ typeof checkoutHead !== 'string' || !/^[a-f0-9]{40}$/.test(checkoutHead) || selectedCommit !== checkoutHead) {
196
+ throw new Error(`Selected source commit must equal the current integration checkout HEAD (selected=${selectedCommit ?? '<missing>'}, HEAD=${checkoutHead ?? '<missing>'})`);
197
+ }
198
+ return true;
199
+ }
200
+
201
+ export function resolveFixtureSourceRoot(plugin, executionRoot) {
202
+ return plugin ? resolve(plugin, '../..') : resolve(executionRoot);
203
+ }
204
+
205
+ export function assertSnapshotProvenance(provenance, { verifyArtifactFiles = false } = {}) {
206
+ for (const key of ['binarySha256', 'binaryVersion', 'sourceCommit', 'sourceCheckoutHead', 'sourceTree',
207
+ 'sourceSnapshotSha256', 'packageLockSha256', 'dependencyNodeModulesRealpath',
208
+ 'installedPackageLockSha256', 'sourceComponentSha256', 'harnessComponentSha256', 'sourceArchiveSha256',
209
+ 'executionSnapshotTar', 'executionSnapshotSha256', 'sourceSnapshotTar', 'binary', 'dependencyInstallSha256', 'dependencyInstallRealpath',
210
+ 'sourceCheckoutStatusSha256']) {
211
+ if (typeof provenance?.[key] !== 'string' || !provenance[key]) throw new Error(`Missing provenance field: ${key}`);
212
+ }
213
+ if (typeof provenance.sourceCheckoutDirtyStatus !== 'string' || typeof provenance.harnessDirtyStatus !== 'string') {
214
+ throw new Error('Missing checkout dirty-state status provenance');
215
+ }
216
+ if (provenance.binaryVersion !== '2.0.32' || !/^[a-f0-9]{64}$/.test(provenance.binarySha256) ||
217
+ !/^[a-f0-9]{40}$/.test(provenance.sourceCommit) || provenance.sourceCheckoutHead !== provenance.sourceCommit ||
218
+ !/^[a-f0-9]{40}$/.test(provenance.sourceTree) ||
219
+ !/^[a-f0-9]{64}$/.test(provenance.sourceSnapshotSha256) || !/^[a-f0-9]{64}$/.test(provenance.packageLockSha256) ||
220
+ !/^[a-f0-9]{64}$/.test(provenance.sourceComponentSha256) || !/^[a-f0-9]{64}$/.test(provenance.harnessComponentSha256) ||
221
+ !/^[a-f0-9]{64}$/.test(provenance.sourceArchiveSha256) ||
222
+ !/^[a-f0-9]{64}$/.test(provenance.executionSnapshotSha256) ||
223
+ !/^[a-f0-9]{64}$/.test(provenance.dependencyInstallSha256) ||
224
+ !/^[a-f0-9]{64}$/.test(provenance.sourceCheckoutStatusSha256) ||
225
+ !provenance.dependencyNodeModulesRealpath.startsWith('/') || !provenance.dependencyInstallRealpath.startsWith('/')) {
226
+ throw new Error('Pinned binary or source snapshot provenance is invalid');
227
+ }
228
+ if (typeof provenance.sourceDirty !== 'boolean' || typeof provenance.sourceCheckoutDirty !== 'boolean' ||
229
+ provenance.sourceCheckoutDirty !== (provenance.sourceCheckoutDirtyStatus.length > 0) ||
230
+ createHash('sha256').update(provenance.sourceCheckoutDirtyStatus).digest('hex') !== provenance.sourceCheckoutStatusSha256 ||
231
+ typeof provenance.harnessDirty !== 'boolean' ||
232
+ provenance.harnessDirty !== (provenance.harnessDirtyStatus.length > 0)) {
233
+ throw new Error('Checkout dirty-state provenance is invalid');
234
+ }
235
+ if (provenance.sourceDirty !== false) throw new Error('Source snapshot must be created from a clean immutable commit');
236
+ const dependencyPackages = provenance.dependencyPackages;
237
+ if (!dependencyPackages || typeof dependencyPackages !== 'object') throw new Error('Pinned dependency package provenance is missing');
238
+ for (const name of ['@opencode/cli', '@opencode/client', '@opencode/plugin']) {
239
+ const item = dependencyPackages[name];
240
+ if (item?.version !== '2.0.32' || typeof item.realpath !== 'string' || !item.realpath.startsWith('/') ||
241
+ !/^[a-f0-9]{64}$/.test(item.packageJsonSha256 ?? '') || !/^[a-f0-9]{64}$/.test(item.treeSha256 ?? '')) {
242
+ throw new Error(`Pinned dependency provenance is invalid for ${name}`);
243
+ }
244
+ }
245
+ if (provenance.installedPackageLockSha256 !== provenance.packageLockSha256) {
246
+ throw new Error('Installed dependencies are not identical to the selected source lockfile');
247
+ }
248
+ if (verifyArtifactFiles) {
249
+ const binaryPath = provenance.binary;
250
+ const sourceTar = provenance.sourceSnapshotTar;
251
+ const executionTar = provenance.executionSnapshotTar;
252
+ for (const [label, path, expected] of [
253
+ ['binary', binaryPath, provenance.binarySha256],
254
+ ['source archive', sourceTar, provenance.sourceArchiveSha256],
255
+ ['execution snapshot', executionTar, provenance.executionSnapshotSha256],
256
+ ]) {
257
+ if (typeof path !== 'string' || !existsSync(path) || createHash('sha256').update(readFileSync(path)).digest('hex') !== expected) {
258
+ throw new Error(`${label} artifact hash does not match manifest provenance`);
259
+ }
260
+ }
261
+ const dependencyInstall = hashDependencyInstall(provenance.dependencyNodeModulesRealpath);
262
+ if (dependencyInstall.treeSha256 !== provenance.dependencyInstallSha256 || dependencyInstall.realpath !== provenance.dependencyInstallRealpath) {
263
+ throw new Error('dependency install artifact hash does not match manifest provenance');
264
+ }
265
+ }
266
+ return true;
267
+ }
268
+
269
+ export function verifySnapshotIdentity(expected, observed, { verifyArtifactFiles = true } = {}) {
270
+ assertSnapshotProvenance(observed, { verifyArtifactFiles });
271
+ for (const key of ['binarySha256', 'binaryVersion', 'sourceCommit', 'sourceCheckoutHead', 'sourceTree',
272
+ 'sourceDirty', 'sourceCheckoutDirty', 'sourceCheckoutDirtyStatus', 'sourceCheckoutStatusSha256', 'harnessDirty', 'harnessDirtyStatus',
273
+ 'sourceSnapshotSha256', 'packageLockSha256', 'dependencyNodeModulesRealpath',
274
+ 'installedPackageLockSha256', 'sourceComponentSha256', 'harnessComponentSha256', 'sourceArchiveSha256',
275
+ 'executionSnapshotTar', 'executionSnapshotSha256', 'sourceSnapshotTar', 'binary', 'dependencyInstallSha256', 'dependencyInstallRealpath']) {
276
+ if (expected?.[key] !== observed[key]) throw new Error(`Snapshot identity mismatch for ${key}`);
277
+ }
278
+ if (JSON.stringify(expected?.dependencyPackages) !== JSON.stringify(observed.dependencyPackages)) {
279
+ throw new Error('Snapshot identity mismatch for dependencyPackages');
280
+ }
281
+ return true;
282
+ }
@@ -0,0 +1,72 @@
1
+ /** Serialize paired-run provenance with JSON's typed, escaped representation. */
2
+ import { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { createHash } from 'node:crypto';
5
+ export function createPairedManifest(fields) {
6
+ const sourceCheckoutDirtyStatus = fields.sourceCheckoutDirtyStatus ?? '';
7
+ const harnessDirtyStatus = fields.harnessDirtyStatus ?? '';
8
+ const manifest = {
9
+ createdAt: fields.createdAt,
10
+ binary: fields.binary,
11
+ binaryVersion: fields.binaryVersion,
12
+ compatibilityVersion: fields.compatibilityVersion ?? fields.binaryVersion,
13
+ runtimeVersion: fields.runtimeVersion ?? fields.binaryVersion,
14
+ runtimeSourceRevision: fields.runtimeSourceRevision,
15
+ runtimePatchSha256: fields.runtimePatchSha256,
16
+ runtimeLockSha256: fields.runtimeLockSha256,
17
+ binarySha256: fields.binarySha256,
18
+ expectedHostVersion: '2.0.32',
19
+ sourceCheckout: fields.sourceCheckout,
20
+ sourceCheckoutHead: fields.sourceCheckoutHead,
21
+ sourceCheckoutDirty: sourceCheckoutDirtyStatus.length > 0,
22
+ sourceCheckoutDirtyStatus,
23
+ sourceCheckoutStatusSha256: createHash('sha256').update(sourceCheckoutDirtyStatus).digest('hex'),
24
+ sourceCommit: fields.sourceCommit,
25
+ sourceTree: fields.sourceTree,
26
+ sourceDirty: false,
27
+ sourceSnapshotTar: fields.sourceSnapshotTar,
28
+ sourceArchiveSha256: fields.sourceArchiveSha256,
29
+ executionSnapshotTar: fields.executionSnapshotTar,
30
+ executionSnapshotSha256: fields.executionSnapshotSha256,
31
+ sourceSnapshotSha256: fields.sourceSnapshotSha256,
32
+ packageLockSha256: fields.packageLockSha256,
33
+ installedPackageLockSha256: fields.installedPackageLockSha256,
34
+ dependencyNodeModulesRealpath: fields.dependencyNodeModulesRealpath,
35
+ dependencyInstallSha256: fields.dependencyInstallSha256,
36
+ dependencyInstallRealpath: fields.dependencyInstallRealpath,
37
+ dependencyPackages: fields.dependencyPackages,
38
+ harnessWorktree: fields.harnessWorktree,
39
+ harnessCommit: fields.harnessCommit,
40
+ harnessDirty: harnessDirtyStatus.length > 0,
41
+ harnessDirtyStatus,
42
+ harnessSourceManifestSha256: fields.harnessSourceManifestSha256,
43
+ order: ['baseline-1', 'ctg-1', 'ctg-2', 'baseline-2'],
44
+ samplesPerRun: fields.samplesPerRun,
45
+ warmup: 'one input key excluded; both switch destinations visited before measured alternating switches',
46
+ terminal: '36 rows x 120 columns',
47
+ thresholds: { inputP95Ms: 50, switchP95Ms: 150, workspaceRenderMs: 300 },
48
+ measurementScope: 'keypress or Enter through select/read/VT parsing to parsed PTY screen evidence; not compositor/GPU presentation; no observer-overhead subtraction',
49
+ hostSampling: 'one-second load-average and MemAvailable observations throughout each run\'s switch/input window',
50
+ pairedConditions: 'one condition server at a time; fresh disposable profiles/databases with equivalent same-project session titles/prompts/markers; same pinned binary and 36x120 PTY; CTG loaded from complete selected source archive',
51
+ pluginRequirement: 'the raw native plugin.list inventory must show carthagent active plus health RPC for CTG; no carthagent id or namespace for baseline; state is observed, never inferred from the mode flag',
52
+ identityRequirement: 'binaryVersion and compatibilityVersion are the exact compatibility contract 2.0.32; runtimeVersion is captured separately from exact native runtime --version and exact server.info.version. Owned binaries require loadNativeTestRuntime with the trusted release manifest (default lib/opencode/runtime-release.json or CTG_TEST_RUNTIME_RELEASE_MANIFEST) and optional CTG_TEST_RUNTIME_SHA256; adjacent ctg-runtime.json is provenance only, never the trust source.',
53
+ sustainedMemoryAcceptance: 'plan proposal only, not authorized: serial baseline and CTG each warm 5 minutes, then collect exactly 1500 planned 1-second monotonic-index slots over 25 minutes. Record actual monotonic timestamps/lateness; >250ms lateness, duplicate/missing slots, missing process/PID/sample fail; no zero-fill/interpolation/drop; align by slot index not jittered timestamp. Block 0=slots 0-299; final block=1200-1499. Combined process-tree RSS B0/C0=condition medians in block 0; D[n]=(CTG[n]-C0)-(baseline[n]-B0). Proposed retained-growth max D<=128 MiB; proposed observable-plateau median(D[1200..1499])-median(D[0..299])<=32 MiB. Preserve separate TUI/server/combined traces and slot timestamps/lateness. RSS sums may double-count shared pages; this is a process-tree RSS proxy, not unique physical memory. Require exact finite deterministic loopback replay specification with fixed request count, payload/chunk sizes, pacing and reconnect positions, identical byte-for-byte between conditions. Engineering finite-window criteria, not mathematical long-term guarantees; review before authorization.',
54
+ workspaceUiAcceptance: 'the native compatibility target includes cached /ctg-workspace UI load <=300ms; a strict post-submit PTY route/data-render observation is required, RPC-only timing does not satisfy it',
55
+ sustainedMemoryDiagnosticDesign: 'plan only; use indexed monotonic one-second slots with <=250ms lateness, 1500 slots, fixed blocks 0-299 and 1200-1499; fail on missing PID/process/sample, duplicate/missing slot, excessive lateness, or mismatched deterministic loopback replay. Retain TUI/server/combined RSS, slot timestamps/lateness, host load/memory, per-request bytes/chunks/timing, reconnect/error counts and cleanup. Proposed combined-RSS statistic and 128/32 MiB criteria are specified in sustainedMemoryAcceptance and require review and explicit authorization before execution.',
56
+ sourceComponentSha256: fields.sourceComponentSha256,
57
+ harnessComponentSha256: fields.harnessComponentSha256,
58
+ acceptance: 'diagnostic until operator confirms controlled window and independent review; thresholds unchanged',
59
+ };
60
+ return manifest;
61
+ }
62
+
63
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
64
+ const input = await new Promise((resolve, reject) => {
65
+ let value = '';
66
+ process.stdin.setEncoding('utf8');
67
+ process.stdin.on('data', chunk => { value += chunk; });
68
+ process.stdin.on('end', () => resolve(value));
69
+ process.stdin.on('error', reject);
70
+ });
71
+ process.stdout.write(`${JSON.stringify(createPairedManifest(JSON.parse(input)), null, 2)}\n`);
72
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Shared ABBA driver used by the paired runner and its offline orchestration test.
4
+ register_paired_exit_cleanup() {
5
+ trap "$1" EXIT
6
+ }
7
+
8
+ remove_paired_workdir() {
9
+ local path="${1:-}"
10
+ if [[ "$path" == /tmp/ctg-native-paired.* && "$path" != *..* ]]; then
11
+ rm -rf -- "$path"
12
+ fi
13
+ }
14
+
15
+ run_paired_abba() {
16
+ local callback="$1" statuses_file="$2" guard_module="$3" interrupted_flag="$4"
17
+ local label mode status exit_status
18
+ for label in baseline-1 ctg-1 ctg-2 baseline-2; do
19
+ case "$label" in
20
+ baseline-*) mode=baseline ;;
21
+ ctg-*) mode=ctg ;;
22
+ esac
23
+ set +e
24
+ "$callback" "$label" "$mode"
25
+ status=$?
26
+ set -e
27
+ # A callback may own richer failure reporting, but it cannot erase its
28
+ # process failure by having already recorded a success status.
29
+ if [[ "$status" -ne 0 ]]; then
30
+ local status_tmp="${statuses_file}.update.$$"
31
+ if ! awk -F '\t' -v name="$label" -v failed_status="$status" 'BEGIN { OFS="\t" } $1 == name { $2=failed_status; found=1 } { print } END { if (!found) print name, failed_status }' \
32
+ "$statuses_file" > "$status_tmp" 2>/dev/null; then
33
+ printf '%s\t%s\n' "$label" "$status" > "$status_tmp"
34
+ fi
35
+ mv -- "$status_tmp" "$statuses_file"
36
+ elif ! awk -F '\t' -v name="$label" '$1 == name { found=1 } END { exit !found }' "$statuses_file" 2>/dev/null; then
37
+ printf '%s\t1\n' "$label" >> "$statuses_file"
38
+ fi
39
+ done
40
+ paired_exit_status="$(node --input-type=module - "$statuses_file" "$guard_module" "$interrupted_flag" <<'NODE'
41
+ import { readFileSync } from 'node:fs';
42
+ import { pathToFileURL } from 'node:url';
43
+ const { aggregateConditionExit } = await import(pathToFileURL(process.argv[3]).href);
44
+ const statuses = Object.fromEntries(readFileSync(process.argv[2], 'utf8').trim().split('\n').filter(Boolean).map(line => {
45
+ const [name, status] = line.split('\t');
46
+ return [name, Number(status)];
47
+ }));
48
+ process.stdout.write(String(aggregateConditionExit(statuses, process.argv[4] === '1')));
49
+ NODE
50
+ )"
51
+ }
@@ -0,0 +1,33 @@
1
+ /** Wait for plugin initialization, preserving the host inventory as evidence. */
2
+ export async function waitForPluginActive(pluginID, {
3
+ listPlugins,
4
+ timeoutMs = 15_000,
5
+ hostOutput = () => '',
6
+ pollIntervalMs = 100,
7
+ }) {
8
+ const deadline = Date.now() + timeoutMs;
9
+ let inventory;
10
+ while (true) {
11
+ const remainingMs = deadline - Date.now();
12
+ if (remainingMs <= 0) {
13
+ throw new Error(`Timed out waiting for plugin ${pluginID} to become active; observed ${JSON.stringify(inventory?.data ?? [])}\nHost output:\n${hostOutput()}`);
14
+ }
15
+ try {
16
+ inventory = await listPlugins(remainingMs);
17
+ } catch (error) {
18
+ if (Date.now() >= deadline) {
19
+ throw new Error(`Timed out waiting for plugin ${pluginID} to become active; last observed inventory ${JSON.stringify(inventory?.data ?? [])}; inventory request failed: ${error.message}\nHost output:\n${hostOutput()}`, { cause: error });
20
+ }
21
+ throw error;
22
+ }
23
+ const plugin = inventory.data.find((entry) => entry.id === pluginID);
24
+ if (plugin?.state?.status === 'active') return plugin;
25
+ if (plugin?.state?.status === 'error') {
26
+ throw new Error(`Plugin ${pluginID} failed to initialize: ${JSON.stringify(plugin)}\nHost output:\n${hostOutput()}`);
27
+ }
28
+ if (Date.now() >= deadline) {
29
+ throw new Error(`Timed out waiting for plugin ${pluginID} to become active; observed ${JSON.stringify(inventory.data)}\nHost output:\n${hostOutput()}`);
30
+ }
31
+ await new Promise((done) => setTimeout(done, Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()))));
32
+ }
33
+ }
@@ -0,0 +1,53 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { waitForPluginActive } from './native-plugin-readiness.mjs';
4
+
5
+ test('waits through plugin visibility and loading until active', async () => {
6
+ const inventories = [
7
+ { data: [] },
8
+ { data: [{ id: 'carthagent', state: { status: 'loading' } }] },
9
+ { data: [{ id: 'carthagent', state: { status: 'active' } }] },
10
+ ];
11
+ let calls = 0;
12
+ const plugin = await waitForPluginActive('carthagent', {
13
+ listPlugins: async () => inventories[calls++],
14
+ pollIntervalMs: 1,
15
+ });
16
+ assert.equal(plugin.state.status, 'active');
17
+ assert.equal(calls, 3);
18
+ });
19
+
20
+ test('fails immediately with plugin and host diagnostics on initialization error', async () => {
21
+ let calls = 0;
22
+ await assert.rejects(waitForPluginActive('carthagent', {
23
+ listPlugins: async () => {
24
+ calls++;
25
+ return { data: [{ id: 'carthagent', state: { status: 'error', error: 'fixture failure' } }] };
26
+ },
27
+ hostOutput: () => 'host tail fixture',
28
+ }), /carthagent.*error.*fixture failure.*host tail fixture/s);
29
+ assert.equal(calls, 1);
30
+ });
31
+
32
+ test('times out with the last observed plugin states and host diagnostics', async () => {
33
+ await assert.rejects(waitForPluginActive('carthagent', {
34
+ listPlugins: async () => ({ data: [{ id: 'carthagent', state: { status: 'loading' } }] }),
35
+ timeoutMs: 5,
36
+ pollIntervalMs: 1,
37
+ hostOutput: () => 'host tail fixture',
38
+ }), /Timed out.*carthagent.*loading.*host tail fixture/s);
39
+ });
40
+
41
+ test('bounds an in-flight inventory request by readiness deadline', async () => {
42
+ let observedTimeout;
43
+ await assert.rejects(waitForPluginActive('carthagent', {
44
+ listPlugins: async timeoutMs => {
45
+ observedTimeout = timeoutMs;
46
+ await new Promise(done => setTimeout(done, 11));
47
+ throw new DOMException('request timeout', 'TimeoutError');
48
+ },
49
+ timeoutMs: 10,
50
+ hostOutput: () => 'host tail fixture',
51
+ }), /Timed out.*inventory request failed.*host tail fixture/s);
52
+ assert.ok(observedTimeout > 0 && observedTimeout <= 10);
53
+ });
@@ -0,0 +1,26 @@
1
+ /** Build the Python PTY arguments while preserving the identity gate's digest. */
2
+ export function nativePtySmokeArgs({ root, binary, plugin, selection, probe }) {
3
+ if (!/^[a-f0-9]{64}$/.test(selection.binarySha256 ?? '')) {
4
+ throw new Error('Native PTY selection is missing its independently verified lowercase SHA-256 digest.');
5
+ }
6
+ const args = [
7
+ `${root}/tools/native-pty-smoke.py`, '--binary', binary, '--plugin', plugin,
8
+ '--command', probe.command, '--expect', probe.expect, '--ready', '◆ CTG',
9
+ ...(probe.secondEnter ? ['--second-enter'] : ['--confirm']),
10
+ ];
11
+ for (const next of probe.next ?? []) args.push('--after-command', next.command, '--after-expect', next.expect);
12
+ if (probe.dismissCompletion) args.push('--dismiss-completion');
13
+ if (probe.postSubmitMarker) args.push('--post-submit-marker', probe.postSubmitMarker);
14
+ if (probe.capture) args.push('--capture', probe.capture);
15
+ if (probe.color) args.push('--expect-color', probe.color);
16
+ if (probe.initialTheme) args.push('--initial-theme', probe.initialTheme);
17
+ if (probe.nodeExecutable) args.push('--node-executable', probe.nodeExecutable);
18
+ if (probe.commandArgs) args.push('--command-args', probe.commandArgs);
19
+ if (probe.expectedTheme) args.push('--expect-theme', probe.expectedTheme);
20
+ if (probe.expectStartupColor) args.push('--expect-startup-color');
21
+ if (probe.writeInitialTheme) args.push('--write-initial-theme');
22
+ if (probe.stateDir) args.push('--state-dir', probe.stateDir);
23
+ if (probe.reuseState) args.push('--reuse-state');
24
+ args.push('--expected-sha256', selection.binarySha256);
25
+ return args;
26
+ }