carthagent 1.0.7 → 1.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/beta-rewards-e2e-plan.md +48 -0
  4. package/docs/beta-rewards-e2e-results.md +84 -0
  5. package/docs/cloud-onboarding-acceptance.md +65 -0
  6. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  7. package/docs/glyph-review/test-isolation-v1.md +26 -0
  8. package/docs/glyph-review/typecheck-base.log +1 -0
  9. package/docs/glyph-review/typecheck-v4.log +1 -0
  10. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  11. package/docs/help-dispatch-review.md +69 -0
  12. package/docs/launch-readiness-live-results.md +65 -0
  13. package/docs/legacy-cli.md +150 -0
  14. package/docs/live-theme-plugin-api.md +18 -0
  15. package/docs/native-cli.md +102 -0
  16. package/docs/native-compatibility.md +16 -0
  17. package/docs/native-daily-quests.md +132 -0
  18. package/docs/native-glyph-pty.md +59 -0
  19. package/docs/native-manual-testing.md +336 -0
  20. package/docs/native-slash-acceptance-handoff.md +49 -0
  21. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  22. package/docs/native-terminal-performance.md +310 -0
  23. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  24. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  25. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  34. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  35. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  36. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  37. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  38. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  39. package/docs/native-terminal-semantics.md +89 -0
  40. package/docs/native-upgrade.md +175 -0
  41. package/docs/opencode-composed-source-review.md +89 -0
  42. package/docs/opencode-migration.md +225 -9
  43. package/docs/opencode-owned-runtime.md +241 -0
  44. package/docs/opencode-runtime.md +12 -0
  45. package/docs/opencode-theme-provider-proof/README.md +55 -0
  46. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  47. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  48. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  49. package/docs/owned-host-controls-proof.md +39 -0
  50. package/docs/owned-runtime-release-manifest.md +42 -0
  51. package/docs/owned-visual-clear-proof.md +40 -0
  52. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  53. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  54. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  55. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  61. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  62. package/docs/release-notes-beta.1.md +60 -0
  63. package/docs/release-notes-beta.2.md +60 -0
  64. package/docs/release-notes-native-draft.md +73 -0
  65. package/docs/release-notes-preview.10.md +121 -0
  66. package/docs/release-notes-preview.11.md +76 -0
  67. package/docs/release-notes-preview.12.md +55 -0
  68. package/docs/release-notes-preview.13.md +68 -0
  69. package/docs/release-notes-preview.9.md +130 -0
  70. package/docs/source-preview.md +120 -0
  71. package/evaluate.mjs +28 -1
  72. package/lib/budget.mjs +1 -1
  73. package/lib/cloud/client.mjs +13 -7
  74. package/lib/codegraph.mjs +1 -1
  75. package/lib/delivery-review.mjs +19 -6
  76. package/lib/delivery.mjs +3 -2
  77. package/lib/environment.mjs +8 -0
  78. package/lib/guidance.mjs +1 -1
  79. package/lib/opencode/cli-runtime.mjs +1188 -0
  80. package/lib/opencode/cli.mjs +253 -0
  81. package/lib/opencode/credentials.mjs +63 -8
  82. package/lib/opencode/demo.mjs +53 -0
  83. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  84. package/lib/opencode/evaluation.mjs +32 -12
  85. package/lib/opencode/headless.mjs +22 -5
  86. package/lib/opencode/provider-policy.mjs +23 -0
  87. package/lib/opencode/review.mjs +37 -16
  88. package/lib/opencode/runtime-artifact.mjs +335 -0
  89. package/lib/opencode/runtime-github-transport.mjs +121 -0
  90. package/lib/opencode/runtime-release.json +36 -0
  91. package/lib/opencode/runtime.mjs +299 -20
  92. package/lib/opencode/service.mjs +77 -35
  93. package/lib/opencode/session-options.mjs +6 -2
  94. package/lib/opencode/update.mjs +222 -0
  95. package/lib/opencode/workspace.mjs +1 -1
  96. package/lib/planning-advice.mjs +2 -2
  97. package/lib/providers/experiential.mjs +2 -2
  98. package/lib/providers/names.mjs +1 -1
  99. package/lib/tui/framing.mjs +1 -80
  100. package/lib/tui/models.mjs +1 -1
  101. package/lib/update.mjs +10 -1
  102. package/package.json +33 -7
  103. package/plugins/carthagent/README.md +23 -0
  104. package/plugins/carthagent/src/bootstrap.ts +1 -1
  105. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  106. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  107. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  108. package/plugins/carthagent/src/index.ts +71 -24
  109. package/plugins/carthagent/src/rpc.ts +51 -6
  110. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  111. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  112. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  113. package/plugins/carthagent/src/server/delivery.ts +227 -112
  114. package/plugins/carthagent/src/server/framing.mjs +80 -0
  115. package/plugins/carthagent/src/server/policy.ts +1 -1
  116. package/plugins/carthagent/src/server/providers.ts +65 -36
  117. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  118. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  119. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  120. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  121. package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
  122. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  123. package/plugins/carthagent/src/testing.ts +5 -0
  124. package/plugins/carthagent/src/tui.tsx +506 -0
  125. package/plugins/carthagent/src/ui/client.ts +127 -0
  126. package/plugins/carthagent/src/ui/commands.ts +70 -0
  127. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  128. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  129. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  130. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  131. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  132. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  133. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  134. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  135. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  136. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  137. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  138. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  139. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  140. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  141. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  142. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  143. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  144. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  145. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  146. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  147. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  148. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  149. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  150. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  151. package/plugins/carthagent/tests/commands.test.ts +107 -0
  152. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  153. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  154. package/plugins/carthagent/themes/carthage.json +67 -0
  155. package/plugins/carthagent/tui.ts +26 -1
  156. package/script/ctg/build-owned-runtime.sh +166 -0
  157. package/tests/budget-engine.test.mjs +4 -3
  158. package/tests/cli.test.mjs +52 -96
  159. package/tests/cloud-client.test.mjs +2 -2
  160. package/tests/daily-evidence.test.mjs +62 -0
  161. package/tests/delivery-review.test.mjs +57 -2
  162. package/tests/delivery.test.mjs +22 -1
  163. package/tests/experiential-extension.test.mjs +2 -2
  164. package/tests/experiential.test.mjs +1 -1
  165. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  166. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  167. package/tests/installed-runtime-contract.test.mjs +43 -0
  168. package/tests/native-cli-host-smoke.mjs +457 -0
  169. package/tests/native-daily-editor-focus.test.mjs +213 -0
  170. package/tests/native-daily-editor.test.mjs +43 -0
  171. package/tests/native-daily-quests-layout.test.mjs +55 -0
  172. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  173. package/tests/native-daily-quests-ui.test.mjs +778 -0
  174. package/tests/native-daily-source.test.mjs +525 -0
  175. package/tests/native-demo-ui.test.mjs +48 -0
  176. package/tests/native-evaluation-admission.test.mjs +24 -0
  177. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  178. package/tests/native-fixture-runtime.test.mjs +111 -0
  179. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  180. package/tests/native-glyph-pty.test.mjs +107 -0
  181. package/tests/native-manual-projects.test.mjs +170 -0
  182. package/tests/native-model-command.test.mjs +67 -0
  183. package/tests/native-owned-runtime-env.test.mjs +140 -0
  184. package/tests/native-paired-guards.test.mjs +464 -0
  185. package/tests/native-provider-policy.test.mjs +33 -0
  186. package/tests/native-pty-invocation.test.mjs +114 -0
  187. package/tests/native-pty-runtime.test.mjs +45 -0
  188. package/tests/native-resume-session.test.mjs +46 -0
  189. package/tests/native-review-consumer.test.mjs +286 -0
  190. package/tests/native-runtime-identity.test.mjs +214 -0
  191. package/tests/native-select-mouse.test.mjs +23 -0
  192. package/tests/native-ship-guidance.test.mjs +16 -0
  193. package/tests/native-ship-parity.test.mjs +526 -0
  194. package/tests/native-ship-plan.test.mjs +85 -0
  195. package/tests/native-ship-rpc.test.mjs +83 -0
  196. package/tests/native-ui-commands.test.mjs +6 -0
  197. package/tests/native-voyages-layout.test.mjs +28 -0
  198. package/tests/native-voyages-navigation.test.mjs +169 -0
  199. package/tests/native-voyages-rpc.test.mjs +397 -0
  200. package/tests/native-voyages-ui.test.mjs +307 -0
  201. package/tests/opencode-atomic-write.test.mjs +55 -0
  202. package/tests/opencode-cli.test.mjs +1312 -0
  203. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  204. package/tests/opencode-delivery.test.mjs +16 -0
  205. package/tests/opencode-providers.test.mjs +248 -20
  206. package/tests/opencode-runtime.test.mjs +597 -8
  207. package/tests/opencode-session-controls.test.mjs +15 -0
  208. package/tests/opencode-ship-rpc.test.mjs +71 -0
  209. package/tests/opencode-update.test.mjs +475 -0
  210. package/tests/opencode-workspace.test.mjs +51 -2
  211. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  212. package/tests/release-workflow.test.mjs +46 -0
  213. package/tests/runtime-artifact.test.mjs +269 -0
  214. package/tests/runtime-development-trust.test.mjs +231 -0
  215. package/tests/runtime-github-transport.test.mjs +132 -0
  216. package/tests/runtime-token-env.test.mjs +23 -0
  217. package/tests/server.test.mjs +3 -1
  218. package/tests/ship-telemetry.test.mjs +1 -1
  219. package/tests/source-preview.test.mjs +253 -0
  220. package/tests/test_native_terminal_probe.py +76 -0
  221. package/tests/test_native_terminal_semantics.py +232 -0
  222. package/tests/tui.test.mjs +2 -2
  223. package/tests/update.test.mjs +42 -5
  224. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  225. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  226. package/tests/workspace-live.test.mjs +127 -0
  227. package/tools/benchmark/diagnose-request.mjs +59 -36
  228. package/tools/benchmark/eval-common.mjs +3 -3
  229. package/tools/benchmark/evaluators.test.mjs +4 -4
  230. package/tools/benchmark/guard.test.mjs +35 -2
  231. package/tools/benchmark/guard.ts +20 -7
  232. package/tools/benchmark/planning-startup.test.mjs +30 -34
  233. package/tools/benchmark/runner.mjs +12 -7
  234. package/tools/benchmark/runner.test.mjs +23 -7
  235. package/tools/benchmark/upstream-error.test.mjs +16 -2
  236. package/tools/generate-owned-runtime-release.mjs +217 -0
  237. package/tools/installed-runtime-contract.mjs +16 -0
  238. package/tools/manual-runtime-manifest.mjs +39 -0
  239. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  240. package/tools/native-fixture-stop-diagnostics.mjs +45 -0
  241. package/tools/native-fixture.mjs +199 -40
  242. package/tools/native-glyph-helpers.py +127 -0
  243. package/tools/native-glyph-pty.py +144 -0
  244. package/tools/native-manual-projects.mjs +259 -0
  245. package/tools/native-manual.mjs +338 -0
  246. package/tools/native-model-picker-acceptance.py +306 -0
  247. package/tools/native-owned-runtime-env.sh +47 -0
  248. package/tools/native-paired-guards.mjs +282 -0
  249. package/tools/native-paired-manifest.mjs +72 -0
  250. package/tools/native-paired-orchestration.sh +51 -0
  251. package/tools/native-plugin-readiness.mjs +33 -0
  252. package/tools/native-plugin-readiness.test.mjs +53 -0
  253. package/tools/native-pty-invocation.mjs +26 -0
  254. package/tools/native-pty-runtime.mjs +83 -0
  255. package/tools/native-pty-smoke.py +147 -24
  256. package/tools/native-public-repo-shell-result.mjs +13 -0
  257. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  258. package/tools/native-runtime-identity.mjs +201 -0
  259. package/tools/native-slash-acceptance-env.py +38 -0
  260. package/tools/native-terminal-semantics.py +882 -0
  261. package/tools/owned-shell-observer/index.ts +16 -0
  262. package/tools/owned-shell-observer/package.json +1 -0
  263. package/tools/owned-visual-clear-env.py +48 -0
  264. package/tools/owned-visual-clear-model.py +21 -0
  265. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  266. package/tools/prepare-native-manual.mjs +122 -0
  267. package/tools/run-native-terminal-paired.sh +403 -0
  268. package/tools/runtime-token-env.mjs +8 -0
  269. package/tools/seed-native-switch-fixture.mjs +79 -0
  270. package/tools/test-native-controls.mjs +3 -1
  271. package/tools/test-native-daily-quests.mjs +65 -0
  272. package/tools/test-native-default-cli.mjs +106 -0
  273. package/tools/test-native-delivery-host.mjs +102 -20
  274. package/tools/test-native-evaluation.mjs +7 -3
  275. package/tools/test-native-glyph-pty.mjs +94 -0
  276. package/tools/test-native-host.mjs +91 -65
  277. package/tools/test-native-outcomes.mjs +3 -1
  278. package/tools/test-native-package.mjs +206 -38
  279. package/tools/test-native-performance.mjs +26 -0
  280. package/tools/test-native-product.mjs +90 -0
  281. package/tools/test-native-pty.mjs +34 -9
  282. package/tools/test-native-public-repo.mjs +224 -0
  283. package/tools/test-native-refresh-reentry.mjs +106 -0
  284. package/tools/test-native-report.mjs +27 -20
  285. package/tools/test-native-review.mjs +75 -0
  286. package/tools/test-native-runtime.mjs +120 -0
  287. package/tools/test-native-ship-models.mjs +176 -0
  288. package/tools/test-native-ship-walkthrough.mjs +96 -0
  289. package/tools/test-native-slash-commands.mjs +125 -0
  290. package/tools/test-native-slash-commands.py +242 -0
  291. package/tools/test-native-swe-host.mjs +224 -55
  292. package/tools/test-native-tasks.mjs +77 -0
  293. package/tools/test-native-terminal-performance.py +887 -0
  294. package/tools/test-native-terminal-readiness.sh +85 -0
  295. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  296. package/tools/test-native-visual-clear-installed.mjs +48 -0
  297. package/tools/test-owned-host-controls.mjs +238 -0
  298. package/tools/test-owned-opencode-runtime.mjs +35 -0
  299. package/tools/test-owned-visual-clear-env.py +120 -0
  300. package/tools/test-owned-visual-clear.py +373 -0
  301. package/tools/verify-draft-runtime-gate.mjs +138 -0
  302. package/tools/verify-owned-runtime-release.mjs +114 -0
  303. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Reproducible native build for CTG's separately named OpenCode v2.0.32 runtime.
5
+ # Run from a clean checkout of the locked source revision with Bun 1.4.2.
6
+ ROOT="$(git rev-parse --show-toplevel)"
7
+ EXPECTED_SOURCE="843a6c514e606b436a7fe713422ace0a93ad3d61"
8
+ EXPECTED_LOCK="9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d"
9
+ EXPECTED_PATCH="ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975"
10
+ TARGET="${1:-opencode-linux-x64}"
11
+ HOST_PLATFORM="$(node -p 'process.platform')"
12
+ HOST_ARCH="$(node -p 'process.arch')"
13
+
14
+ cd "$ROOT"
15
+ if command -v sha256sum >/dev/null 2>&1; then
16
+ HASH_COMMAND=(sha256sum)
17
+ elif command -v shasum >/dev/null 2>&1; then
18
+ HASH_COMMAND=(shasum -a 256)
19
+ else
20
+ echo "Need sha256sum or shasum to verify pinned inputs" >&2
21
+ exit 1
22
+ fi
23
+ if [[ "$(git rev-parse HEAD)" != "$EXPECTED_SOURCE" ]]; then
24
+ echo "Expected pristine OpenCode source $EXPECTED_SOURCE; got $(git rev-parse HEAD)" >&2
25
+ exit 1
26
+ fi
27
+ # Reject staged edits and every tracked worktree edit other than a bun.lock
28
+ # line-ending conversion before restoring anything. On Windows, validate that
29
+ # normalizing CRLF to LF exactly reproduces the committed blob; this keeps
30
+ # checkout conversion distinct from user edits and fails closed on content
31
+ # changes. The blob itself must still match the pinned lock hash.
32
+ if ! git diff --cached --quiet HEAD --; then
33
+ echo "Refusing to build with staged source changes" >&2
34
+ exit 1
35
+ fi
36
+ expected_inputs=(
37
+ "docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch"
38
+ "script/ctg/build-owned-runtime.sh"
39
+ )
40
+ actual_status="$(git status --porcelain --untracked-files=all)"
41
+ unexpected=""
42
+ while IFS= read -r entry; do
43
+ [[ -z "$entry" ]] && continue
44
+ path="${entry:3}"
45
+ allowed=false
46
+ for input in "${expected_inputs[@]}"; do
47
+ [[ "$path" == "$input" ]] && allowed=true
48
+ done
49
+ if [[ "$allowed" != true && "$path" != "bun.lock" ]]; then
50
+ unexpected+="${entry}"$'\n'
51
+ fi
52
+ done <<< "$actual_status"
53
+ if [[ -n "$unexpected" ]]; then
54
+ echo "Expected pristine tracked source and only the two reviewed CTG build inputs as untracked files" >&2
55
+ git status --short >&2
56
+ exit 1
57
+ fi
58
+ if ! git diff --quiet HEAD -- . ':(exclude)bun.lock'; then
59
+ echo "Refusing to build with modified tracked source files" >&2
60
+ git status --short >&2
61
+ exit 1
62
+ fi
63
+ node - "$EXPECTED_LOCK" <<'NODE'
64
+ const { createHash } = require("node:crypto")
65
+ const { execFileSync } = require("node:child_process")
66
+ const { readFileSync } = require("node:fs")
67
+ const expectedHash = process.argv[2]
68
+ const committed = execFileSync("git", ["show", "HEAD:bun.lock"])
69
+ const current = readFileSync("bun.lock")
70
+ const normalized = Buffer.from(current.toString("utf8").replace(/\r\n/g, "\n"))
71
+ const hash = createHash("sha256").update(committed).digest("hex")
72
+ if (hash !== expectedHash || !normalized.equals(committed)) {
73
+ console.error("Refusing to restore bun.lock: tracked content differs from the pinned blob")
74
+ process.exit(1)
75
+ }
76
+ NODE
77
+ # Only after the fail-closed pristine check, restore Windows checkout line
78
+ # endings with conversion disabled so Bun sees the exact committed bytes.
79
+ git -c core.autocrlf=false checkout-index --force -- bun.lock
80
+ if [[ "$("${HASH_COMMAND[@]}" bun.lock | awk '{print $1}')" != "$EXPECTED_LOCK" ]]; then
81
+ echo "Pinned bun.lock checksum mismatch" >&2
82
+ exit 1
83
+ fi
84
+ if [[ "$(bun --version)" != "1.4.2" ]]; then
85
+ echo "Expected Bun 1.4.2; got $(bun --version)" >&2
86
+ exit 1
87
+ fi
88
+
89
+ patch_file="$ROOT/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch"
90
+ actual_patch="$("${HASH_COMMAND[@]}" "$patch_file" | awk '{print $1}')"
91
+ if [[ "$actual_patch" != "$EXPECTED_PATCH" ]]; then
92
+ echo "Patch checksum mismatch: $actual_patch" >&2
93
+ exit 1
94
+ fi
95
+ git apply --check "$patch_file"
96
+ git apply "$patch_file"
97
+
98
+ echo "source=$(git rev-parse HEAD)"
99
+ echo "patch_sha256=$actual_patch"
100
+ echo "license=$(grep -m1 '"license"' packages/cli/package.json | cut -d '"' -f4)"
101
+
102
+ bun install --frozen-lockfile
103
+ bun run --cwd packages/core typecheck
104
+ bun run --cwd packages/server typecheck
105
+ bun run --cwd packages/plugin typecheck
106
+ bun run --cwd packages/client typecheck
107
+ bun run --cwd packages/tui typecheck
108
+ bun test --cwd packages/core test/session-compaction.test.ts
109
+ bun test --cwd packages/core test/shell-retention.test.ts
110
+ bun test --cwd packages/server test/shell-provenance.test.ts
111
+ bun test --cwd packages/tui test/plugin-source.test.ts
112
+ bun test --cwd packages/tui test/clear-session.test.tsx
113
+ bun test --cwd packages/tui --timeout 30000 test/navigation-rail.test.tsx --rerun-each 10
114
+ bun test --cwd packages/tui --timeout 30000 test/component/seascape-render.test.tsx --rerun-each 10
115
+ bun test --cwd packages/tui --timeout 30000 test/app-lifecycle.test.tsx test/session-panel.test.tsx
116
+ bun test --cwd packages/tui test/util/renderer.test.ts
117
+ bun test --cwd packages/tui test/component/plan-view.test.tsx test/component/transcript-scrollbox.test.ts test/component/seascape.test.ts test/component/session-tabs-rail.test.tsx test/component/session-tabs-marquee.test.ts test/component/session-tabs-mouse.test.tsx test/context/session-tabs-model.test.ts test/ui/layout.test.ts
118
+ bun test --cwd packages/tui --timeout 60000 test/glyphs.test.ts test/plugin-keymap.test.tsx test/plugin-theme.test.tsx test/plugin-theme-provider.test.tsx
119
+
120
+ cd packages/cli
121
+ # A CTG build is intentionally detached from upstream updater channels and
122
+ # receives an explicit non-upstream identity; it must never masquerade as 2.0.32.
123
+ if [[ "$TARGET" == opencode-windows-* ]]; then
124
+ BUILD_OS=win32
125
+ MANIFEST_PLATFORM="windows-${TARGET#opencode-windows-}"
126
+ BINARY=opencode.exe
127
+ elif [[ "$TARGET" == opencode-darwin-* ]]; then
128
+ BUILD_OS=darwin
129
+ MANIFEST_PLATFORM="darwin-${TARGET#opencode-darwin-}"
130
+ BINARY=opencode
131
+ else
132
+ BUILD_OS=linux
133
+ MANIFEST_PLATFORM="linux-${TARGET#opencode-linux-}"
134
+ BINARY=opencode
135
+ fi
136
+ BUILD_ARCH="$(sed -E 's/^opencode-(linux|darwin|windows)-([^-]+).*$/\2/' <<< "$TARGET")"
137
+ if [[ "$BUILD_OS" != "$HOST_PLATFORM" || "$BUILD_ARCH" != "$HOST_ARCH" ]]; then
138
+ echo "Cross-compiling target $TARGET is not supported by this recipe; run on matching native CI (host $HOST_PLATFORM-$HOST_ARCH)" >&2
139
+ exit 1
140
+ fi
141
+ OPENCODE_VERSION=0.0.0-ctg.2.0.32 OPENCODE_CHANNEL=ctg.2.0.32 \
142
+ bun run script/build.ts --single --target="$TARGET" --skip-web-ui --outdir="$ROOT/packages/cli/dist-ctg"
143
+
144
+ cd "$ROOT"
145
+ artifact="${TARGET#opencode-}"
146
+ binary="$ROOT/packages/cli/dist-ctg/cli-$artifact/bin/$BINARY"
147
+ test -x "$binary"
148
+ "$binary" --version
149
+ cp "$ROOT/LICENSE" "$ROOT/packages/cli/dist-ctg/LICENSE"
150
+ relative_binary="cli-$artifact/bin/$BINARY"
151
+ cd "$ROOT/packages/cli/dist-ctg"
152
+ "${HASH_COMMAND[@]}" "$relative_binary" | tee SHA256SUMS
153
+ cat > ctg-runtime.json <<EOF
154
+ {
155
+ "owner": "Carthagent",
156
+ "sourceRepository": "https://github.com/anomalyco/opencode",
157
+ "sourceRevision": "$EXPECTED_SOURCE",
158
+ "lockSha256": "$EXPECTED_LOCK",
159
+ "patchSha256": "$EXPECTED_PATCH",
160
+ "compatibilityVersion": "2.0.32",
161
+ "runtimeVersion": "0.0.0-ctg.2.0.32",
162
+ "channel": "ctg.2.0.32",
163
+ "platform": "$MANIFEST_PLATFORM",
164
+ "webUI": false
165
+ }
166
+ EOF
@@ -6,8 +6,9 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'nod
6
6
  import { tmpdir } from 'node:os';
7
7
  import { join } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
+ import { buildEngineArgs } from '../lib/engine.mjs';
9
10
 
10
- for (const mode of ['tools', 'seconds', 'repairs']) test(`actual engine stops ${mode}, restores budget and accepts explicit reset offline`, { timeout: 120000 }, async t => {
11
+ for (const mode of ['tools', 'seconds', 'repairs']) test(`legacy engine boundary stops ${mode}, restores budget and accepts explicit reset offline`, { timeout: 120000 }, async t => {
11
12
  const cwd = mkdtempSync(join(tmpdir(), 'budget café '));
12
13
  t.after(() => rmSync(cwd, { recursive: true, force: true }));
13
14
  let calls = 0;
@@ -36,7 +37,7 @@ for (const mode of ['tools', 'seconds', 'repairs']) test(`actual engine stops ${
36
37
  writeFileSync(join(agent, 'models.json'), JSON.stringify({ providers: { localtest: { baseUrl: `http://127.0.0.1:${server.address().port}/v1`, api: 'openai-completions', apiKey: 'dummy', models: [{ id: 'fixture', contextWindow: 128000, maxTokens: 1024 }] } } }));
37
38
  writeFileSync(join(cwd, 'input.txt'), 'café ✓');
38
39
  const session = join(cwd, 'session.jsonl');
39
- const cli = fileURLToPath(new URL('../bin/ctg.mjs', import.meta.url));
40
+ const cli = fileURLToPath(new URL('../vendor/agent/cli.js', import.meta.url));
40
41
  let beforeRestore = -1;
41
42
  let forcedKill = false;
42
43
  async function run(prompt, secondsLimit = mode === 'seconds' ? '5' : '10000', expectBudgetStop = false) {
@@ -44,7 +45,7 @@ for (const mode of ['tools', 'seconds', 'repairs']) test(`actual engine stops ${
44
45
  let priorEntryCount = 0;
45
46
  try { priorEntryCount = readFileSync(session, 'utf8').split('\n').filter(Boolean).length; } catch { /* Initial session has not been created yet. */ }
46
47
  let budgetStoppedAt = null;
47
- const child = spawn(process.execPath, [cli, '-p', '--no-guide', '--provider', 'localtest', '--model', 'fixture', '--session', session, '--max-tools', mode === 'tools' ? '1' : '0', '--max-seconds', secondsLimit, '--max-repairs', mode === 'repairs' ? '1' : '0', prompt], { cwd, env: { ...process.env, PI_OFFLINE: '1', CARTHAGENT_CODING_AGENT_DIR: agent, PI_CODING_AGENT_DIR: agent } });
48
+ const child = spawn(process.execPath, [cli, '--offline', '-p', ...buildEngineArgs({ isolate: true, provider: 'localtest', model: 'fixture', session, maxTools: mode === 'tools' ? '1' : '0', maxSeconds: secondsLimit, maxRepairs: mode === 'repairs' ? '1' : '0' }), prompt], { cwd, env: { ...process.env, PI_OFFLINE: '1', CARTHAGENT_CODING_AGENT_DIR: agent, PI_CODING_AGENT_DIR: agent } });
48
49
  child.stdin.end();
49
50
  let output = ''; child.stdout.on('data', d => output += d); child.stderr.on('data', d => output += d);
50
51
  let timer;
@@ -1,8 +1,8 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync, chmodSync, copyFileSync } from 'node:fs';
3
+ import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
- import { dirname, join, resolve, delimiter } from 'node:path';
5
+ import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
  import { spawnSync } from 'node:child_process';
8
8
  import { approveFixture } from './helpers/tested-design.mjs';
@@ -23,6 +23,31 @@ function run(cwd, ...args) {
23
23
  return spawnSync(process.execPath, [cli, ...args], { cwd, encoding: 'utf8' });
24
24
  }
25
25
 
26
+ test('default CLI and its native alias always use the native console, even with the old opt-out variable', t => {
27
+ const cwd = fixture(t);
28
+ for (const prefix of [[], ['native']]) {
29
+ const result = spawnSync(process.execPath, [cli, ...prefix, '--help'], { cwd, encoding: 'utf8', env: { ...process.env, CTG_NATIVE: '0' } });
30
+ assert.equal(result.status, 0, result.stderr);
31
+ assert.match(result.stdout, /Launch the native Carthagent delivery console/);
32
+ assert.doesNotMatch(result.stdout, /split-terminal|opt-in native|live plan\.d2 side panel/);
33
+ assert.match(result.stdout, /ctg service stop/);
34
+ }
35
+ });
36
+
37
+ test('native runtime selection never falls back to a legacy engine override', t => {
38
+ const cwd = fixture(t);
39
+ const missing = join(cwd, 'missing-native-runtime');
40
+ const fallback = join(cwd, 'legacy.mjs');
41
+ const marker = join(cwd, 'legacy-started');
42
+ writeFileSync(fallback, `import { writeFileSync } from 'node:fs'; writeFileSync(${JSON.stringify(marker)}, 'unexpected');`);
43
+ const result = spawnSync(process.execPath, [cli, '-p', '--engine-cli', missing, '--no-delivery', '--max-tools', '1', 'Task'], {
44
+ cwd, encoding: 'utf8', timeout: 10000, env: { ...process.env, CTG_NATIVE: '0', CARTHAGENT_CLI: fallback },
45
+ });
46
+ assert.equal(result.status, 1, result.stderr);
47
+ assert.match(result.stderr, /missing-native-runtime/);
48
+ assert.throws(() => readFileSync(marker), { code: 'ENOENT' });
49
+ });
50
+
26
51
  test('status reads the newest persisted extension report', t => {
27
52
  const cwd = fixture(t);
28
53
  createReport(cwd, {
@@ -86,13 +111,17 @@ test('guidance routing is not a user-facing command and Ship account commands ar
86
111
  assert.match(result.stdout, /logout-cloud/);
87
112
  });
88
113
 
89
- test('Cloud account commands fail closed when no browser session is stored', t => {
114
+ test('native account commands fail closed instead of consuming legacy engine credentials', t => {
90
115
  const cwd = fixture(t);
91
- const env = { ...process.env, CARTHAGENT_CODING_AGENT_DIR: join(cwd, 'agent'), PI_CODING_AGENT_DIR: join(cwd, 'agent') };
116
+ const agent = join(cwd, 'legacy-agent');
117
+ mkdirSync(agent);
118
+ writeFileSync(join(agent, 'auth.json'), JSON.stringify({ 'experiential-labs': { type: 'api_key', key: 'legacy-fixture-credential' } }));
119
+ const env = { ...process.env, CTG_OPENCODE_BIN: join(cwd, 'missing-native-runtime'), CARTHAGENT_CODING_AGENT_DIR: agent, PI_CODING_AGENT_DIR: agent };
92
120
  for (const args of [['account'], ['billing'], ['logout-cloud']]) {
93
121
  const result = spawnSync(process.execPath, [cli, ...args], { cwd, env, encoding: 'utf8' });
94
122
  assert.equal(result.status, 1, result.stderr);
95
- assert.match(result.stderr, /not signed in/);
123
+ assert.match(result.stderr, /missing-native-runtime/);
124
+ assert.doesNotMatch(result.stdout + result.stderr, /legacy-fixture-credential/);
96
125
  }
97
126
  });
98
127
 
@@ -107,69 +136,12 @@ test('review shows and persists the self-review default', t => {
107
136
  assert.equal(runEnv('review', 'bogus').status, 2);
108
137
  });
109
138
 
110
- /** Fake `gh` + fake engine CLI so `ctg review <pr>` runs end-to-end offline. */
111
- function reviewFixture(t, verdictLine) {
139
+ test('native review rejects invalid targets and bounds before resolving a host', t => {
112
140
  const cwd = fixture(t);
113
- const bin = join(cwd, 'bin');
114
- mkdirSync(join(cwd, 'bin'), { recursive: true });
115
- const gh = join(bin, 'gh');
116
- writeFileSync(gh, `#!/bin/sh
117
- if [ "$1" = "pr" ] && [ "$2" = "view" ]; then echo '{"title":"Fixture PR","url":"https://example.test/pr/14","headRefName":"feat","baseRefName":"main","headRefOid":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}'; exit 0; fi
118
- exit 0
119
- `);
120
- chmodSync(gh, 0o755);
121
- // Native executable fixture: Windows cannot execute a POSIX shebang shim.
122
- if (process.platform === 'win32') {
123
- copyFileSync(process.execPath, join(bin, 'gh.exe'));
124
- writeFileSync(join(cwd, 'gh-preload.cjs'), `if (require('node:path').basename(process.execPath).toLowerCase() === 'gh.exe') { console.log(JSON.stringify({title:'Fixture PR',url:'https://example.test/pr/14',headRefName:'feat',baseRefName:'main',headRefOid:'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'})); process.exit(0); }`);
125
- }
126
- const reviewer = join(cwd, 'reviewer.mjs');
127
- writeFileSync(reviewer, `const prompt = process.argv.at(-1); const snapshot = /snapshot:"([a-f0-9]+)"/.exec(prompt)?.[1]; const head = /head:"([a-f0-9]+)"/.exec(prompt)?.[1]; console.log('fixture findings'); console.log('REVIEW_JSON: ' + JSON.stringify({version:1,snapshot,head,findings:${JSON.stringify(verdictLine.includes('CHANGES-REQUESTED') ? [{file:'app.mjs',line:1,severity:'blocking',issue:'fixture issue',fix:'fixture fix'}] : [])}})); console.log('${verdictLine}');\n`);
128
- const env = { ...process.env, CARTHAGENT_CONFIG: join(cwd, 'config.json'), CARTHAGENT_CLI: reviewer, PATH: `${bin}${delimiter}${process.env.PATH}` };
129
- if (process.platform === 'win32') env.NODE_OPTIONS = `${env.NODE_OPTIONS || ''} --require ${JSON.stringify(join(cwd, 'gh-preload.cjs'))}`;
130
- return { cwd, env };
131
- }
132
-
133
- test('ctg review <pr> exits 0 when the fresh reviewer approves', t => {
134
- const { cwd, env } = reviewFixture(t, 'VERDICT: APPROVE');
135
- const result = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
136
- assert.equal(result.status, 0, result.stderr);
137
- assert.match(result.stdout, /fixture findings/);
138
- assert.match(result.stdout, /VERDICT: APPROVE/);
139
- });
140
-
141
- test('ctg review <pr> exits 1 on requested changes and 2 without a verdict', t => {
142
- const { cwd, env } = reviewFixture(t, 'VERDICT: CHANGES-REQUESTED');
143
- const result = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
144
- assert.equal(result.status, 1, result.stderr);
145
- const silent = reviewFixture(t, 'analysis but no verdict line');
146
- const bad = spawnSync(process.execPath, [cli, 'review', '14'], { cwd: silent.cwd, env: silent.env, encoding: 'utf8' });
147
- assert.equal(bad.status, 2);
148
- assert.match(bad.stderr, /no VERDICT/);
149
- });
150
-
151
- test('review persists admission limits across fresh CLI processes', t => {
152
- const { cwd, env } = reviewFixture(t, 'VERDICT: CHANGES-REQUESTED');
153
- for (let i = 0; i < 3; i++) {
154
- const result = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
155
- assert.equal(result.status, 1, result.stderr);
156
- }
157
- const exhausted = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
158
- assert.equal(exhausted.status, 2);
159
- assert.match(exhausted.stderr, /round limit/);
160
- assert.doesNotMatch(exhausted.stdout, /fixture findings/);
161
- });
162
-
163
- test('review rejects source mutation and invalid execution bounds', t => {
164
- const { cwd, env } = reviewFixture(t, 'VERDICT: APPROVE');
165
- writeFileSync(join(cwd, 'reviewer.mjs'), `import {writeFileSync} from 'node:fs'; writeFileSync('changed.txt', 'café'); console.log('VERDICT: APPROVE');`);
166
- const changed = spawnSync(process.execPath, [cli, 'review', '14'], { cwd, env, encoding: 'utf8' });
167
- assert.equal(changed.status, 2);
168
- assert.match(changed.stderr, /snapshot changed/);
169
- for (const value of ['0', '301', 'Infinity', 'invalid']) {
170
- const invalid = spawnSync(process.execPath, [cli, 'review', '14', '--timeout', value], { cwd, env, encoding: 'utf8' });
171
- assert.equal(invalid.status, 2);
172
- assert.match(invalid.stderr, /integer from 1 to 300/);
141
+ for (const args of [['review', 'bogus'], ['review', '14', '--timeout', '0'], ['review', '14', '--timeout', '301'], ['review', '14', '--timeout', 'Infinity'], ['review', '14', '--timeout', 'invalid']]) {
142
+ const result = run(cwd, ...args);
143
+ assert.equal(result.status, 2, result.stderr);
144
+ assert.match(result.stderr, /Invalid review target|integer from 1 to 300/);
173
145
  }
174
146
  });
175
147
 
@@ -194,33 +166,17 @@ test('review modes launch the real bundled engine without unknown flags', t => {
194
166
  }
195
167
  });
196
168
 
197
- test('ctg --list-models renders stored identity-only Cloud metadata as unknown', t => {
169
+ test('native model listing cannot silently fall back to legacy cached model metadata', t => {
198
170
  const cwd = fixture(t);
199
- const agentDir = join(cwd, 'agent');
200
- mkdirSync(agentDir, { recursive: true });
201
- writeFileSync(join(agentDir, 'auth.json'), JSON.stringify({
202
- 'experiential-labs': { type: 'api_key', key: 'fixture-credential' },
203
- }));
204
- writeFileSync(join(agentDir, 'models-store.json'), JSON.stringify({
205
- 'experiential-labs': {
206
- models: [{ id: 'carthagent-code', name: 'carthagent-code' }],
207
- checkedAt: 1,
208
- },
209
- }));
210
- const result = spawnSync(process.execPath, [cli, '--list-models', 'Carthagent Ship'], {
211
- cwd,
212
- encoding: 'utf8',
213
- env: {
214
- ...process.env,
215
- PI_OFFLINE: '1',
216
- CARTHAGENT_CODING_AGENT_DIR: agentDir,
217
- PI_CODING_AGENT_DIR: agentDir,
218
- },
219
- });
220
- assert.equal(result.status, 0, result.stderr);
221
- assert.match(result.stdout, /Carthagent Ship\s+carthagent-code\s+\?\s+\?\s+\?\s+\?/);
222
- assert.doesNotMatch(result.stdout, /experiential/i);
223
- assert.doesNotMatch(result.stderr, /Cannot read properties|Failed to load extension/);
171
+ const agent = join(cwd, 'legacy-agent');
172
+ mkdirSync(agent);
173
+ writeFileSync(join(agent, 'models-store.json'), JSON.stringify({ 'experiential-labs': { models: [{ id: 'legacy-only-model' }], checkedAt: 1 } }));
174
+ const result = spawnSync(process.execPath, [cli, '--list-models'], { cwd, encoding: 'utf8', env: {
175
+ ...process.env, CTG_OPENCODE_BIN: join(cwd, 'missing-native-runtime'), CARTHAGENT_CODING_AGENT_DIR: agent, PI_CODING_AGENT_DIR: agent,
176
+ } });
177
+ assert.equal(result.status, 1, result.stderr);
178
+ assert.match(result.stderr, /missing-native-runtime/);
179
+ assert.doesNotMatch(result.stdout, /legacy-only-model/);
224
180
  });
225
181
 
226
182
  test('vendored agent engine is bundled and loads ModelRuntime cleanly', async () => {
@@ -235,11 +191,11 @@ test('vendored agent engine is bundled and loads ModelRuntime cleanly', async ()
235
191
  assert.ok(providers.size >= 35);
236
192
  });
237
193
 
238
- test('CLI rejects invalid, missing and disabled-extension budget options', t => {
194
+ test('native CLI rejects invalid and missing budget options', t => {
239
195
  const cwd = fixture(t);
240
- for (const args of [['--max-tools'], ['--max-seconds', 'NaN', 'task'], ['--no-delivery', '--max-tools', '1', 'task']]) {
196
+ for (const args of [['--max-tools'], ['--max-seconds', 'NaN', 'task'], ['--no-delivery', '--max-tools', '-1', 'task']]) {
241
197
  const result = spawnSync(process.execPath, [cli, '-p', ...args], { cwd, encoding: 'utf8', timeout: 10000 });
242
198
  assert.equal(result.status, 1, result.stderr);
243
- assert.match(result.stderr, /Missing value|must be an integer|require the delivery/);
199
+ assert.match(result.stderr, /Missing value|must be an integer|non-negative/);
244
200
  }
245
201
  });
@@ -90,7 +90,7 @@ test('Cloud client sends account, billing, and session requests with bearer auth
90
90
 
91
91
  test('OAuth and account formatters keep token expiration and account UX deterministic', () => {
92
92
  assert.deepEqual(oauthCredentials({ accessToken: 'a', refreshToken: 'r', expiresIn: 900 }, 1_000), { access: 'a', refresh: 'r', expires: 901_000 });
93
- assert.match(formatCloudAccount({ accountId: 'acct', balance: { availableNanoUsd: 1_000_000_000 }, subscription: { status: 'none' }, sessions: [{}, { revoked_at: 'x' }] }), /Credit: \$1\.00 available[\s\S]*Plan: Free[\s\S]*1 active, 1 revoked/);
93
+ assert.match(formatCloudAccount({ accountId: 'acct', balance: { availableNanoUsd: 1_000_000_000 }, subscription: { status: 'none' }, sessions: [{}, { revoked_at: 'x' }] }), /Credit: \$1\.00 available[\s\S]*Plan: No paid plan[\s\S]*1 active, 1 revoked/);
94
94
  const managed = formatCloudAccount({
95
95
  accountId: 'paid', balance: { availableNanoUsd: 8_000_000_000 },
96
96
  subscription: { status: 'active', current_period_end: 1_800_000_000 },
@@ -123,7 +123,7 @@ test('OAuth and account formatters keep token expiration and account UX determin
123
123
  test('managed usage recovery distinguishes daily, monthly, subscription, and credit paths', () => {
124
124
  assert.match(cloudManagedUsageRecovery('daily_limit_reached'), /00:00 UTC/);
125
125
  assert.match(cloudManagedUsageRecovery('monthly_allowance_exhausted'), /billing portal/);
126
- assert.match(cloudManagedUsageRecovery({ code: 'subscription_inactive' }), /subscription is inactive/);
126
+ assert.match(cloudManagedUsageRecovery({ code: 'subscription_inactive' }), /awaiting payment or activation/);
127
127
  assert.match(cloudManagedUsageRecovery('insufficient_quota'), /billing checkout/);
128
128
  assert.equal(cloudManagedUsageRecovery('upstream timeout'), null);
129
129
  });
@@ -0,0 +1,62 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { canonicalDailyEvidence, dailyRequest } from '../plugins/carthagent/src/daily-evidence.mjs';
4
+
5
+ const ID = 'ABCDEF01-2345-6789-ABCD-EF0123456789';
6
+ const REV = 'A'.repeat(40);
7
+ const evidence = () => ({ projectId: ID, outcomeId: ID, runId: ID, revision: REV, goal: ' A goal\r\nline ', change: 'A change', checks: [{ name: 'test', before: 'failed', after: 'passed', revision: REV, result: 'Observed passing check' }], findings: [{ issue: 'Issue', evidence: 'Observation', resolution: 'Resolved' }] });
8
+ const invalid = value => assert.throws(() => canonicalDailyEvidence(value), { message: 'invalid_daily_evidence' });
9
+ test('canonical evidence has backend field ordering, lowercase identifiers, LF and trim; preserves astral pairs', () => {
10
+ const e = evidence(); e.change = '\r valid \u{1f680} \r';
11
+ const c = canonicalDailyEvidence(e);
12
+ assert.equal(c.goal, 'A goal\nline'); assert.equal(c.change, 'valid \u{1f680}');
13
+ assert.equal(c.projectId, ID.toLowerCase()); assert.equal(c.revision, REV.toLowerCase());
14
+ assert.deepEqual(Object.keys(c), ['projectId', 'outcomeId', 'runId', 'revision', 'goal', 'change', 'checks', 'findings']);
15
+ assert.deepEqual(canonicalDailyEvidence(c), c);
16
+ assert.deepEqual(dailyRequest(ID, e), { id: ID.toLowerCase(), consent: true, evidence: c });
17
+ assert.throws(() => dailyRequest('bad-id', e), { message: 'invalid_daily_evidence' });
18
+ });
19
+ test('exact fields, plain data records, dense arrays, limits and enums fail closed', () => {
20
+ for (const mutate of [
21
+ e => { e.extra = true; }, e => { delete e.runId; }, e => { e.checks[0].extra = true; }, e => { delete e.findings[0].issue; },
22
+ e => { e.projectId = 'not-a-uuid'; }, e => { e.revision = 'a'.repeat(41); }, e => { e.checks[0].after = 'ok'; },
23
+ e => { e.checks[0].before = 'passed'; }, e => { e.checks[0].revision = 'b'.repeat(40); },
24
+ e => { e.checks = Array(1); }, e => { e.findings = Array(1); }, e => { e.checks.extra = true; },
25
+ e => { e.checks = Array.from({ length: 13 }, () => e.checks[0]); }, e => { e.findings = Array.from({ length: 6 }, () => e.findings[0]); },
26
+ e => { Object.defineProperty(e, 'goal', { get() { throw new Error('must not read getter'); }, enumerable: true }); },
27
+ e => { e.checks = [Object.create(e.checks[0])]; }, e => { e[Symbol('extra')] = true; },
28
+ ]) { const e = evidence(); mutate(e); invalid(e); }
29
+ const e = evidence(); e.checks[0].after = 'failed'; e.checks[0].revision = 'b'.repeat(64);
30
+ assert.equal(canonicalDailyEvidence(e).checks[0].revision.length, 64);
31
+ });
32
+ test('rejects controls and lone surrogates before normalization and bounds UTF8, not character count', () => {
33
+ for (const value of ['\u0000', '\u000b', '\u001f', '\u007f', '\u0085', '\ud800', '\udfff', 'x\ud800y']) {
34
+ const e = evidence(); e.goal = value; invalid(e);
35
+ }
36
+ for (const [field, limit] of [['goal', 1024], ['change', 3072]]) {
37
+ const e = evidence(); e[field] = ' '.repeat(limit + 1); invalid(e);
38
+ e[field] = 'é'.repeat(limit / 2); assert.equal(canonicalDailyEvidence(e)[field], e[field]);
39
+ e[field] += 'a'; invalid(e);
40
+ }
41
+ for (const [collection, index, field, limit] of [['checks', 0, 'name', 160], ['checks', 0, 'result', 1024], ['findings', 0, 'issue', 768], ['findings', 0, 'evidence', 1536], ['findings', 0, 'resolution', 1024]]) {
42
+ const e = evidence(); e[collection][index][field] = ' '.repeat(limit + 1); invalid(e);
43
+ }
44
+ });
45
+ test('canonical aggregate evidence limit is exact, includes escaping, request is bounded', () => {
46
+ const e = evidence(); e.goal = 'g'.repeat(1024); e.change = 'c'.repeat(3072);
47
+ e.checks = Array.from({ length: 7 }, () => ({ name: 'n', before: 'failed', after: 'passed', revision: REV, result: 'r'.repeat(1024) }));
48
+ e.findings = [];
49
+ while (Buffer.byteLength(JSON.stringify(canonicalDailyEvidence({ ...e, checks: e.checks.slice(0, -1) }))) > 12288) e.checks.pop();
50
+ const base = { ...e, checks: e.checks.slice(0, -1) };
51
+ let last = canonicalDailyEvidence(base);
52
+ // Fill another bounded field to find the precise canonical edge.
53
+ base.findings = [{ issue: '', evidence: '', resolution: '' }];
54
+ for (let n = 0; n <= 1536; n++) {
55
+ base.findings[0].evidence = 'x'.repeat(n);
56
+ try { last = canonicalDailyEvidence(base); } catch {
57
+ assert.equal(Buffer.byteLength(JSON.stringify(last)), 12288); invalid(base);
58
+ assert.ok(Buffer.byteLength(JSON.stringify(dailyRequest(ID, last))) <= 16384); return;
59
+ }
60
+ }
61
+ assert.fail('fixture must reach evidence byte boundary');
62
+ });
@@ -115,7 +115,7 @@ test('review rejects modified receipts, stale coverage, and cancellation', async
115
115
  await assert.rejects(captureDeliveryReview(state, cwd, dir, AbortSignal.abort()), /cancelled/);
116
116
  });
117
117
 
118
- test('review accepts filesystem aliases of the Git root but rejects ancestor repositories', async t => {
118
+ test('review accepts filesystem aliases of the Git root and nested workspaces', async t => {
119
119
  const { cwd, state } = fixture(t);
120
120
  const parent = mkdtempSync(join(tmpdir(), 'review-alias-'));
121
121
  t.after(() => rmSync(parent, { recursive: true, force: true }));
@@ -126,7 +126,51 @@ test('review accepts filesystem aliases of the Git root but rejects ancestor rep
126
126
  assert.match(capture.diff, /diff --git/);
127
127
  const child = join(cwd, 'nested');
128
128
  mkdirSync(child);
129
- await assert.rejects(captureDeliveryReview(state, child, join(child, '.harness')), /workspace Git root/);
129
+ writeFileSync(join(child, 'new.txt'), 'nested source');
130
+ const nested = await captureDeliveryReview(state, child, join(child, '.harness'));
131
+ assert.match(nested.diff, /nested source/);
132
+ assert.doesNotMatch(nested.diff, /export const amount|export const result/);
133
+ });
134
+
135
+ test('nested review scopes tracked, staged, deleted and new files and retains freshness', async t => {
136
+ const { cwd: root, state, params } = fixture(t);
137
+ const cwd = join(root, 'app café [one]');
138
+ mkdirSync(cwd);
139
+ const git = args => execFileSync('git', args, { cwd: root, stdio: 'pipe' });
140
+ for (const name of ['changed.txt', 'deleted.txt', 'moved.txt']) writeFileSync(join(cwd, name), 'baseline\n');
141
+ git(['add', '.']);
142
+ git(['-c', 'user.name=Test', '-c', 'user.email=test@localhost', 'commit', '-qm', 'Nested baseline']);
143
+ writeFileSync(join(root, 'app.mjs'), 'OUTSIDE_TRACKED_SECRET');
144
+ writeFileSync(join(root, 'outside.txt'), 'OUTSIDE_NEW_SECRET');
145
+ writeFileSync(join(cwd, 'changed.txt'), 'inside staged change\n');
146
+ git(['add', '.']);
147
+ git(['mv', 'app café [one]/moved.txt', 'outside-moved.txt']);
148
+ rmSync(join(cwd, 'deleted.txt'));
149
+ writeFileSync(join(cwd, 'new café.txt'), 'inside new source');
150
+ for (const name of ['node_modules', '.harness', 'artifacts']) {
151
+ mkdirSync(join(cwd, name), { recursive: true });
152
+ writeFileSync(join(cwd, name, 'noise.txt'), 'EXCLUDED_SECRET');
153
+ }
154
+ const capture = await captureDeliveryReview(state, cwd, join(cwd, '.harness', 'review'));
155
+ assert.match(capture.diff, /inside staged change/);
156
+ assert.match(capture.diff, /deleted file mode/);
157
+ assert.match(capture.diff, /inside new source/);
158
+ assert.match(capture.diff, /a\/moved.txt/);
159
+ assert.doesNotMatch(capture.diff, /OUTSIDE_|outside-moved|EXCLUDED_SECRET/);
160
+ state.evidence.unit.fingerprint = fingerprint(cwd, ['.']);
161
+ recordDeliveryReview(state, cwd, params(capture.id));
162
+ writeFileSync(join(root, 'outside.txt'), 'outside changes do not stale app review');
163
+ recordDeliveryReview(state, cwd, params(capture.id));
164
+ writeFileSync(join(cwd, 'new café.txt'), 'changed after review');
165
+ assert.throws(() => recordDeliveryReview(state, cwd, params(capture.id)), /stale/);
166
+ });
167
+
168
+ test('nested review rejects untracked symlinks escaping the workspace', async t => {
169
+ const { cwd: root, state } = fixture(t);
170
+ const cwd = join(root, 'nested');
171
+ mkdirSync(cwd);
172
+ symlinkSync(join(root, 'app.mjs'), join(cwd, 'escape.mjs'));
173
+ await assert.rejects(captureDeliveryReview(state, cwd, join(cwd, '.harness')), /outside|escape|symlink/i);
130
174
  });
131
175
 
132
176
  test('large diffs retain full receipt instead of silently using output tail', async t => {
@@ -137,3 +181,14 @@ test('large diffs retain full receipt instead of silently using output tail', as
137
181
  assert.ok(readFileSync(capture.path, 'utf8').length > 12000);
138
182
  assert.match(capture.diff, /diff --git/);
139
183
  });
184
+
185
+ test('review accepts assertion aliases and names missing requirements', async t => {
186
+ const { cwd, state, params, dir } = fixture(t);
187
+ const capture = await captureDeliveryReview(state, cwd, dir);
188
+ assert.throws(() => recordDeliveryReview(state, cwd, { ...params(capture.id), coverage: [{ requirement: 'Other', assertions: 'x' }] }), /Missing: \["Correct amount"\]/);
189
+ recordDeliveryReview(state, cwd, { ...params(capture.id), coverage: [{ requirement: 'Correct amount', assertion: 'app amount equals 2' }] });
190
+ assert.equal(state.reviewEvidence.coverage[0].assertions, 'app amount equals 2');
191
+ const again = await captureDeliveryReview(state, cwd, dir);
192
+ recordDeliveryReview(state, cwd, { ...params(again.id), coverage: [{ requirement: 'Correct amount', assertions: ['amount is 2', 'new result is 2'] }] });
193
+ assert.equal(state.reviewEvidence.coverage[0].assertions, 'amount is 2; new result is 2');
194
+ });
@@ -79,6 +79,28 @@ test('argv execution captures output without shell expansion, including spaces',
79
79
  assert.equal(result.code, 0); assert.match(result.output, /literal \$HOME ; echo bad/);
80
80
  assert.match(readFileSync(logPath, 'utf8'), /literal/);
81
81
  });
82
+ test('runCommand strips runtime transport tokens case-insensitively without mutating parent env', async t => {
83
+ const cwd = fixture(t);
84
+ const tokenKeys = ['CTG_RUNTIME_GITHUB_TOKEN', 'ctg_runtime_github_token', 'CtG_RuNtImE_GiThUb_ToKeN'];
85
+ const original = new Map(tokenKeys.map(key => [key, process.env[key]]));
86
+ const preservedOriginal = process.env.DELIVERY_ENV_PRESERVED;
87
+ process.env.DELIVERY_ENV_PRESERVED = 'yes';
88
+ for (const key of tokenKeys) process.env[key] = 'test-only-marker';
89
+ t.after(() => {
90
+ if (preservedOriginal === undefined) delete process.env.DELIVERY_ENV_PRESERVED;
91
+ else process.env.DELIVERY_ENV_PRESERVED = preservedOriginal;
92
+ for (const [key, value] of original) {
93
+ if (value === undefined) delete process.env[key];
94
+ else process.env[key] = value;
95
+ }
96
+ });
97
+
98
+ const script = `const keys = Object.keys(process.env); console.log(JSON.stringify({ tokensAbsent: !keys.some(key => key.toUpperCase() === 'CTG_RUNTIME_GITHUB_TOKEN'), preserved: process.env.DELIVERY_ENV_PRESERVED === 'yes' }));`;
99
+ const result = await runCommand([process.execPath, '-e', script], { cwd });
100
+ assert.equal(result.code, 0);
101
+ assert.deepEqual(JSON.parse(result.output.trim()), { tokensAbsent: true, preserved: true });
102
+ assert.deepEqual(tokenKeys.map(key => process.env[key]), tokenKeys.map(key => 'test-only-marker'));
103
+ });
82
104
  test('nonzero exit and missing executable cannot be successful', async t => {
83
105
  const cwd = fixture(t);
84
106
  const fail = await runCommand([process.execPath, '-e', 'process.exit(7)'], { cwd, logPath: join(cwd, 'fail.log') });
@@ -340,4 +362,3 @@ test('guidance, prompt template and README document the classification', () => {
340
362
  assert.match(readme, /Informational vs delivery asks/);
341
363
  assert.match(readme, /advisory/);
342
364
  });
343
-
@@ -31,7 +31,7 @@ test('Cloud extension gives limit-specific recovery without hiding BYOK', () =>
31
31
  assert.equal(notices.length, 3);
32
32
  assert.match(notices[0][0], /00:00 UTC/);
33
33
  assert.match(notices[1][0], /billing portal/);
34
- assert.match(notices[2][0], /subscription is inactive/);
34
+ assert.match(notices[2][0], /awaiting payment or activation/);
35
35
  assert.ok(notices.every(([, tone]) => tone === 'warning'));
36
- assert.ok(notices.every(([message]) => /\/login for BYOK/.test(message)));
36
+ assert.ok(notices.every(([message]) => /\/login/.test(message) && /BYOK|your own provider/.test(message)));
37
37
  });
@@ -272,7 +272,7 @@ test('registerExperientialProvider registers one shared provider definition', ()
272
272
  assert.equal(registerExperientialProvider(runtime), runtime);
273
273
  assert.equal(calls.length, 1);
274
274
  assert.equal(calls[0].id, 'experiential-labs');
275
- assert.equal(calls[0].config.name, 'Carthagent Ship');
275
+ assert.equal(calls[0].config.name, 'Carthagent Cloud');
276
276
  });
277
277
 
278
278
  test('ModelRuntime resolves EXPLABS_API_KEY without copying it into provider config', async () => {
@@ -0,0 +1,15 @@
1
+ import { Plugin } from '@opencode/plugin';
2
+
3
+ export default Plugin.define({
4
+ id: 'ctg-project-discovery-fixture',
5
+ setup: async ctx => {
6
+ const registration = await ctx.command.transform(editor => {
7
+ editor.add({
8
+ name: 'fixture-project-plugin-marker',
9
+ description: 'Proves project-local plugin discovery is active.',
10
+ execute: async () => { throw new Error('fixture marker command is not intended to execute'); },
11
+ });
12
+ });
13
+ return () => registration.dispose();
14
+ },
15
+ });