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,41 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { setSessionAutoCompaction } from "../src/ui/session-preferences.mjs";
4
+
5
+ test("autocompact persists explicit, idempotent session preferences without replacing neighbor metadata", async () => {
6
+ let current = { "ctg.autoCompaction": false, "ctg.raw": true, "user.flag": "keep" };
7
+ const updates = [];
8
+ const client = {
9
+ async get(input, options) {
10
+ assert.equal(input.sessionID, "s1");
11
+ assert.equal(options.location.directory, "/project");
12
+ assert.ok(options.signal instanceof AbortSignal);
13
+ return { metadata: current };
14
+ },
15
+ async update(input, options) {
16
+ assert.equal(input.sessionID, "s1");
17
+ assert.equal(options.location.directory, "/project");
18
+ current = input.metadata;
19
+ updates.push(input.metadata);
20
+ },
21
+ };
22
+ const location = { directory: "/project" };
23
+ assert.equal(await setSessionAutoCompaction(client, "s1", location, "off"), false);
24
+ assert.equal(updates.length, 0, "repeating off must not write");
25
+ assert.equal(await setSessionAutoCompaction(client, "s1", location, "on"), true);
26
+ assert.deepEqual(current, { "ctg.raw": true, "user.flag": "keep" });
27
+ assert.equal(await setSessionAutoCompaction(client, "s1", location, "toggle"), false);
28
+ assert.deepEqual(current, { "ctg.autoCompaction": false, "ctg.raw": true, "user.flag": "keep" });
29
+ assert.equal(await setSessionAutoCompaction(client, "s1", location, "toggle"), true);
30
+ assert.equal(updates.length, 3);
31
+ });
32
+
33
+ test("invalid autocompact arguments fail before reading or mutating session state", async () => {
34
+ let calls = 0;
35
+ const client = {
36
+ async get() { calls++; throw new Error("must not read"); },
37
+ async update() { calls++; throw new Error("must not update"); },
38
+ };
39
+ await assert.rejects(setSessionAutoCompaction(client, "s1", { directory: "/project" }, "sometimes"), /Usage: \/autocompact \[on\|off\]/);
40
+ assert.equal(calls, 0);
41
+ });
@@ -0,0 +1,269 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { expect, test } from "bun:test";
3
+ import { testRender } from "@opentui/solid";
4
+ import { ScrollBoxRenderable, SelectRenderable, type Renderable } from "@opentui/core";
5
+ import { createSignal } from "solid-js";
6
+ import { PluginContextProvider } from "@opencode/plugin/tui";
7
+ import type { Context } from "@opencode/plugin/tui/context";
8
+ import { parseThemeDocument, resolveThemeDocument } from "@opencode/theme/tui";
9
+ import theme from "../themes/carthage.json";
10
+ import { Voyages } from "../src/voyages-rpc.js";
11
+ import { VoyagesNavigation } from "../src/ui/voyages-navigation.js";
12
+
13
+ const profile = { profile: { name: "Test captain", shipName: "Test vessel", coins: 10, level: 2, totalXp: 150 } };
14
+ const slate = { slate: { day: "2026-10-30", endsAt: "2026-10-31T00:00:00Z", items: [
15
+ { id: "quest-one", definition: { title: "Protect a boundary" }, status: "open" },
16
+ { id: "quest-two", definition: { title: "Review a change" }, status: "assessed_complete" },
17
+ ] }, records: [{ request: { evidence: "must not appear in navigation", consent: true } }] };
18
+
19
+ async function fixture({ width = 30, height = 36, authenticated = true } = {}) {
20
+ const [active, setActive] = createSignal(true);
21
+ const [focused, setFocused] = createSignal(false);
22
+ const [compact, setCompact] = createSignal(false);
23
+ const [mode, setMode] = createSignal("base");
24
+ const [sessionID, setSessionID] = createSignal<string | undefined>("coding-session");
25
+ const routes: unknown[] = [], calls: string[] = [], selected: string[] = [];
26
+ const responses: Promise<unknown>[] = [];
27
+ let responseGate: Promise<void> | undefined;
28
+ let listener: ((event: { details: { type: string } }) => void) | undefined;
29
+ let listening = true;
30
+ const context = {
31
+ theme: resolveThemeDocument(parseThemeDocument(theme), "dark"),
32
+ location: { directory: "/test-project" },
33
+ data: {
34
+ session: { get: () => ({ location: { directory: "/test-project" } }) },
35
+ location: { default: () => ({ directory: "/test-project" }) },
36
+ listen: (next: typeof listener) => { listener = next; return () => { listening = false; }; },
37
+ },
38
+ client: { rpc: (definition: unknown) => ({ snapshot: () => {
39
+ calls.push(definition === Voyages ? "profile" : "daily");
40
+ const response = authenticated ? (responseGate ?? Promise.resolve()).then(() => definition === Voyages ? profile : slate) : Promise.reject({ type: "voyagesError", data: { code: "authentication_required" } });
41
+ responses.push(response);
42
+ return response;
43
+ } }) },
44
+ keymap: { mode: { current: mode }, dispatch: (id: string) => { calls.push(id); } },
45
+ ui: {
46
+ router: { navigate: (route: unknown) => routes.push(route) },
47
+ navigation: { current: () => "voyages", select: (name: string) => { selected.push(name); return true; } },
48
+ },
49
+ } as unknown as Context;
50
+ const app = await testRender(() => <PluginContextProvider value={context}>
51
+ <box width={width} height={height} visible={active()}>
52
+ <VoyagesNavigation width={width} active={active()} compact={compact()} focused={focused()} sessionID={sessionID()} focus={() => { if (mode() !== "base") return; setFocused(true); setMode("navigation"); }} blur={() => { setFocused(false); if (mode() === "navigation") setMode("base"); }} />
53
+ </box>
54
+ </PluginContextProvider>, { width, height, kittyKeyboard: true });
55
+ const settle = async () => { await Promise.allSettled(responses); await app.renderOnce(); };
56
+ await settle();
57
+ return {
58
+ app, calls, routes, selected, mode, setMode, setActive, setFocused, setCompact, setSessionID, settle,
59
+ focus: async () => { setFocused(true); setMode("navigation"); await app.renderOnce(); },
60
+ event: (type: string) => listener?.({ details: { type } }),
61
+ delay: (promise: Promise<void> | undefined) => { responseGate = promise; },
62
+ listening: () => listening,
63
+ list: () => app.renderer.currentFocusedRenderable as SelectRenderable,
64
+ scroll: () => {
65
+ const find = (node: Renderable): ScrollBoxRenderable | undefined => node instanceof ScrollBoxRenderable ? node : node.getChildren().map(find).find(Boolean);
66
+ return find(app.renderer.root)!;
67
+ },
68
+ };
69
+ }
70
+
71
+ test.each([30, 18])("real Voyages navigator renders account and assignment summaries at width %i", async (width) => {
72
+ const f = await fixture({ width });
73
+ try {
74
+ const frame = f.app.captureCharFrame();
75
+ expect(frame).toContain("Test captain");
76
+ expect(frame).toContain("10 coins");
77
+ expect(frame).toContain("Daily coding");
78
+ expect(frame).not.toContain("must not appear");
79
+ expect(f.calls).toEqual(["profile", "daily"]);
80
+ expect(f.routes).toHaveLength(0);
81
+ } finally { f.app.renderer.destroy(); }
82
+ expect(f.listening()).toBe(false);
83
+ });
84
+
85
+ test("rail focus owns arrows/Enter; palette or modal mode cannot trigger a route", async () => {
86
+ const f = await fixture();
87
+ try {
88
+ expect(f.app.renderer.currentFocusedRenderable).not.toBeInstanceOf(SelectRenderable);
89
+ await f.focus();
90
+ expect(f.list()).toBeInstanceOf(SelectRenderable);
91
+ f.app.mockInput.pressArrow("down");
92
+ await f.app.renderOnce();
93
+ expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
94
+ for (const mode of ["modal", "palette", "base"]) {
95
+ f.setMode(mode);
96
+ await f.app.renderOnce();
97
+ f.app.mockInput.pressEnter();
98
+ await f.app.renderOnce();
99
+ expect(f.routes).toHaveLength(0);
100
+ }
101
+ f.setMode("navigation");
102
+ await f.app.renderOnce();
103
+ f.app.mockInput.pressEnter();
104
+ await f.app.renderOnce();
105
+ expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-one" } }]);
106
+ expect(f.mode()).toBe("base");
107
+ expect(f.calls).toEqual(["profile", "daily"]);
108
+ } finally { f.app.renderer.destroy(); }
109
+ });
110
+
111
+ test("Projects/Voyages toggles retain selection and do not route, submit or purchase", async () => {
112
+ const f = await fixture();
113
+ try {
114
+ await f.focus();
115
+ f.app.mockInput.pressArrow("down");
116
+ await f.app.renderOnce();
117
+ expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
118
+ f.setActive(false);
119
+ await f.settle();
120
+ expect(f.app.captureCharFrame()).not.toContain("Test captain");
121
+ f.setActive(true);
122
+ await f.settle();
123
+ expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
124
+ expect(f.routes).toHaveLength(0);
125
+ expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
126
+ } finally { f.app.renderer.destroy(); }
127
+ });
128
+
129
+ test("compact mode uses the same mounted selection and keyboard behavior", async () => {
130
+ const f = await fixture();
131
+ try {
132
+ await f.focus();
133
+ f.app.mockInput.pressArrow("down");
134
+ await f.app.renderOnce();
135
+ const list = f.list();
136
+ f.setCompact(true);
137
+ f.app.resize(5, 36);
138
+ await f.app.renderOnce();
139
+ expect(f.list()).toBe(list);
140
+ expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
141
+ expect(f.app.captureCharFrame()).not.toContain("Test captain");
142
+ f.app.mockInput.pressEnter();
143
+ await f.app.renderOnce();
144
+ expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-one" } }]);
145
+ } finally { f.app.renderer.destroy(); }
146
+ });
147
+
148
+ test("switching modes preserves the Voyages scroll position at a short terminal height", async () => {
149
+ const f = await fixture({ height: 12 });
150
+ try {
151
+ const scroll = f.scroll();
152
+ scroll.scrollBy(5);
153
+ await f.app.renderOnce();
154
+ const before = scroll.scrollTop;
155
+ expect(before).toBeGreaterThan(0);
156
+ f.setActive(false);
157
+ await f.settle();
158
+ f.setActive(true);
159
+ await f.settle();
160
+ expect(f.scroll()).toBe(scroll);
161
+ expect(scroll.scrollTop).toBe(before);
162
+ expect(f.routes).toHaveLength(0);
163
+ } finally { f.app.renderer.destroy(); }
164
+ });
165
+
166
+ test("slow summary reads preserve scrolled position after an intervening loading layout", async () => {
167
+ const f = await fixture({ height: 12 });
168
+ try {
169
+ const scroll = f.scroll();
170
+ scroll.scrollTo(100);
171
+ await f.app.renderOnce();
172
+ const before = scroll.scrollTop;
173
+ expect(before).toBeGreaterThan(0);
174
+ f.setActive(false);
175
+ await f.app.renderOnce();
176
+ const gate = Promise.withResolvers<void>();
177
+ f.delay(gate.promise);
178
+ f.setActive(true);
179
+ await f.app.renderOnce();
180
+ expect(f.app.captureCharFrame()).not.toContain("Test captain");
181
+ expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
182
+ gate.resolve();
183
+ await f.settle();
184
+ await f.app.renderOnce();
185
+ expect(scroll.scrollTop).toBe(before);
186
+ } finally { f.app.renderer.destroy(); }
187
+ });
188
+
189
+ test("short terminal keyboard navigation keeps the highlighted action on screen", async () => {
190
+ const f = await fixture({ height: 12 });
191
+ try {
192
+ await f.focus();
193
+ for (let index = 0; index < 7; index++) {
194
+ f.app.mockInput.pressArrow("down");
195
+ await f.app.renderOnce();
196
+ }
197
+ expect(f.list().getSelectedOption()?.value).toBe("back");
198
+ expect(f.app.captureCharFrame()).toContain("Return to coding");
199
+ expect(f.list().height).toBeLessThanOrEqual(f.scroll().viewport.height);
200
+ expect(f.routes).toHaveLength(0);
201
+ } finally { f.app.renderer.destroy(); }
202
+ });
203
+
204
+ test("mouse press focuses the rail and release opens the clicked row without acting through a modal", async () => {
205
+ const f = await fixture();
206
+ try {
207
+ const row = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Daily coding quests"));
208
+ expect(row).toBeGreaterThanOrEqual(0);
209
+ for (const mode of ["modal", "palette"]) {
210
+ f.setMode(mode);
211
+ await f.app.renderOnce();
212
+ await f.app.mockMouse.click(5, row);
213
+ await f.app.renderOnce();
214
+ expect(f.mode()).toBe(mode);
215
+ expect(f.routes).toHaveLength(0);
216
+ }
217
+ f.setMode("base");
218
+ f.app.renderer.clearSelection();
219
+ await f.app.renderOnce();
220
+ await f.app.mockMouse.pressDown(5, row);
221
+ await f.app.renderOnce();
222
+ expect(f.mode()).toBe("navigation");
223
+ expect(f.app.renderer.currentFocusedRenderable).toBeInstanceOf(SelectRenderable);
224
+ expect(f.routes).toHaveLength(0);
225
+ await f.app.mockMouse.release(5, row);
226
+ await f.app.renderOnce();
227
+ expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } }]);
228
+ expect(f.mode()).toBe("base");
229
+ const questRow = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Review a change"));
230
+ expect(questRow).toBeGreaterThanOrEqual(0);
231
+ await f.app.mockMouse.click(5, questRow);
232
+ await f.app.renderOnce();
233
+ expect(f.routes).toEqual([
234
+ { type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } },
235
+ { type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-two" } },
236
+ ]);
237
+ } finally { f.app.renderer.destroy(); }
238
+ });
239
+
240
+ test("signed-out account and invalidation hide protected summaries, reset stale selection, and stay read-only", async () => {
241
+ const f = await fixture({ authenticated: false });
242
+ try {
243
+ expect(f.app.captureCharFrame()).toContain("authentication_required");
244
+ expect(f.app.captureCharFrame()).not.toContain("Test captain");
245
+ await f.focus();
246
+ f.app.mockInput.pressArrow("down");
247
+ await f.app.renderOnce();
248
+ f.event("credential.updated");
249
+ await f.settle();
250
+ expect(f.list().getSelectedOption()?.value).toBe("daily");
251
+ expect(f.routes).toHaveLength(0);
252
+ expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
253
+ } finally { f.app.renderer.destroy(); }
254
+ });
255
+
256
+ test.each(["coding-session", undefined])("return to coding explicitly navigates to the selected session or Home (%s)", async (sessionID) => {
257
+ const f = await fixture();
258
+ try {
259
+ f.setSessionID(sessionID);
260
+ await f.focus();
261
+ const list = f.list();
262
+ const index = list.options.findIndex((option) => option.value === "back");
263
+ list.setSelectedIndex(index);
264
+ f.app.mockInput.pressEnter();
265
+ await f.app.renderOnce();
266
+ expect(f.selected).toEqual(["projects"]);
267
+ expect(f.routes).toEqual([sessionID ? { type: "session", sessionID } : { type: "home" }]);
268
+ } finally { f.app.renderer.destroy(); }
269
+ });
@@ -0,0 +1,67 @@
1
+ {
2
+ "$schema": "https://opencode.ai/theme.json",
3
+ "base": {
4
+ "categorical": ["accent", "purple", "cyan", "green", "blue"],
5
+ "text": {
6
+ "base": "$hue.neutral.100",
7
+ "muted": "$hue.neutral.300",
8
+ "faint": "$hue.neutral.400",
9
+ "action": {
10
+ "primary": { "base": "$hue.purple.400", "$hovered": "$hue.purple.300", "$pressed": "$hue.purple.500" },
11
+ "secondary": { "base": "$hue.neutral.200", "$hovered": "$hue.yellow.400" },
12
+ "destructive": { "base": "$hue.red.300", "$hovered": "$hue.red.200" }
13
+ },
14
+ "formfield": { "base": "$hue.neutral.100", "$focused": "$hue.yellow.400" },
15
+ "feedback": {
16
+ "info": { "base": "$hue.cyan.300", "muted": "$hue.cyan.500" },
17
+ "success": { "base": "$hue.green.300", "muted": "$hue.green.500" },
18
+ "warning": { "base": "$hue.yellow.300", "muted": "$hue.yellow.500" },
19
+ "error": { "base": "$hue.red.300", "muted": "$hue.red.500" }
20
+ }
21
+ },
22
+ "background": {
23
+ "base": "$hue.neutral.900",
24
+ "raised": { "base": "$hue.neutral.800", "high": "$hue.neutral.700", "max": "$hue.neutral.600" },
25
+ "action": {
26
+ "primary": { "base": "$hue.yellow.400", "$hovered": "$hue.yellow.300", "$pressed": "$hue.yellow.500" },
27
+ "secondary": { "base": "$hue.neutral.800", "$hovered": "$hue.neutral.700" },
28
+ "destructive": { "base": "$hue.red.700", "$hovered": "$hue.red.600" }
29
+ },
30
+ "formfield": { "base": "$hue.neutral.900", "$focused": "$hue.neutral.800" },
31
+ "feedback": {
32
+ "info": { "base": "$hue.blue.800" }, "success": { "base": "$hue.green.800" },
33
+ "warning": { "base": "$hue.orange.800" }, "error": { "base": "$hue.red.800" }
34
+ }
35
+ },
36
+ "border": { "base": "$hue.neutral.600" },
37
+ "scrollbar": { "base": "$hue.neutral.600" },
38
+ "diff": {
39
+ "text": { "added": "$hue.green.300", "removed": "$hue.red.300", "context": "$hue.neutral.300", "hunkHeader": "$hue.cyan.300" },
40
+ "background": { "added": "$hue.green.800", "removed": "$hue.red.800", "context": "$hue.neutral.800" },
41
+ "highlight": { "added": "$hue.green.700", "removed": "$hue.red.700" },
42
+ "lineNumber": { "text": "$hue.neutral.400", "background": { "added": "$hue.green.900", "removed": "$hue.red.900" } }
43
+ },
44
+ "syntax": {
45
+ "comment": "$hue.neutral.400", "keyword": "$hue.purple.300", "function": "$hue.yellow.300", "variable": "$hue.neutral.100",
46
+ "string": "$hue.green.300", "number": "$hue.orange.300", "type": "$hue.cyan.300", "operator": "$hue.purple.300", "punctuation": "$hue.neutral.300"
47
+ },
48
+ "markdown": {
49
+ "text": "$hue.neutral.100", "heading": "$hue.yellow.300", "link": "$hue.cyan.300", "linkText": "$hue.cyan.300", "code": "$hue.green.300",
50
+ "blockQuote": "$hue.neutral.300", "emphasis": "$hue.orange.300", "strong": "$hue.yellow.300", "horizontalRule": "$hue.neutral.500",
51
+ "listItem": "$hue.yellow.400", "listEnumeration": "$hue.yellow.400", "image": "$hue.purple.300", "imageText": "$hue.neutral.300", "codeBlock": "$hue.neutral.100"
52
+ }
53
+ },
54
+ "dark": {
55
+ "hue": {
56
+ "gray": { "100": "#f4eef5", "200": "#d8cbdc", "300": "#c0b3ca", "400": "#9a8ca6", "500": "#705e7a", "600": "#49364f", "700": "#35243d", "800": "#271733", "900": "#160e20" },
57
+ "red": { "100": "#f6d4dd", "200": "#e8a9ba", "300": "#d58ca2", "400": "#b8758d", "500": "#9c5a73", "600": "#713e55", "700": "#512a40", "800": "#351a2d", "900": "#21101d" },
58
+ "orange": { "100": "#ffead3", "200": "#f8d8b3", "300": "#eec28b", "400": "#d5af66", "500": "#ad8b50", "600": "#80673e", "700": "#5d4a32", "800": "#3b2e28", "900": "#241c21" },
59
+ "yellow": { "100": "#fff4d8", "200": "#f5dca9", "300": "#e6c480", "400": "#d5af66", "500": "#b18c4e", "600": "#84683d", "700": "#604a30", "800": "#3e3026", "900": "#251d20" },
60
+ "green": { "100": "#d9f2df", "200": "#b1e2bd", "300": "#83c996", "400": "#62ad78", "500": "#46885e", "600": "#35684a", "700": "#284d39", "800": "#1e382b", "900": "#17251e" },
61
+ "cyan": { "100": "#d5f2f3", "200": "#a9e0e2", "300": "#83c8ce", "400": "#62aab5", "500": "#4a8997", "600": "#396874", "700": "#2c4c58", "800": "#203640", "900": "#17242b" },
62
+ "blue": { "100": "#dce9fb", "200": "#b9d0f0", "300": "#91b2df", "400": "#7193c6", "500": "#5876a4", "600": "#445b7e", "700": "#34445d", "800": "#263342", "900": "#1a222d" },
63
+ "purple": { "100": "#f0e2f5", "200": "#ddc0e7", "300": "#c99bd7", "400": "#ad7abe", "500": "#895f99", "600": "#694878", "700": "#4d365b", "800": "#352440", "900": "#211629" },
64
+ "accent": "$hue.yellow", "interactive": "$hue.yellow", "neutral": "$hue.gray"
65
+ }
66
+ }
67
+ }
@@ -1 +1,26 @@
1
- export { default } from './src/bootstrap';
1
+ import { Plugin } from '@opencode/plugin/tui';
2
+ import diagnostics from './src/bootstrap';
3
+ import workspace from './src/tui';
4
+
5
+ export default Plugin.define({
6
+ id: 'carthagent.tui',
7
+ async setup(context) {
8
+ const cleanups: Plugin.Cleanup[] = [];
9
+ const dispose = async () => {
10
+ const results = await Promise.allSettled(cleanups.splice(0).reverse().map((cleanup) => Promise.resolve().then(cleanup)));
11
+ const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
12
+ if (failures.length) throw new AggregateError(failures.map((result) => result.reason), 'Carthagent terminal cleanup failed');
13
+ };
14
+ try {
15
+ for (const definition of [diagnostics, workspace]) {
16
+ const cleanup = await definition.setup(context);
17
+ if (cleanup) cleanups.push(cleanup);
18
+ }
19
+ return dispose;
20
+ } catch (error) {
21
+ try { await dispose(); }
22
+ catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Carthagent terminal setup failed'); }
23
+ throw error;
24
+ }
25
+ },
26
+ });
@@ -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;