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,242 @@
1
+ #!/usr/bin/env python3
2
+ """Bounded actual-slash PTY acceptance for the active Carthagent native plugin."""
3
+ import argparse
4
+ import fcntl
5
+ import hashlib
6
+ import importlib.util
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import pty
11
+ import select
12
+ import signal
13
+ import struct
14
+ import subprocess
15
+ import termios
16
+ import time
17
+
18
+ def load_local_module(name, filename):
19
+ spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(filename))
20
+ module = importlib.util.module_from_spec(spec)
21
+ spec.loader.exec_module(module)
22
+ return module
23
+
24
+
25
+ Screen = load_local_module('native_terminal_semantics', 'native-terminal-semantics.py').Screen
26
+ _env = load_local_module('native_slash_acceptance_env', 'native-slash-acceptance-env.py')
27
+ TUI_ENV_KEYS = _env.TUI_ENV_KEYS
28
+ assert_clean_child_environment = _env.assert_clean_child_environment
29
+ fixture_environment = _env.fixture_environment
30
+
31
+
32
+ class Tui:
33
+ def __init__(self, binary, fixture, session_id, env):
34
+ self.master, slave = pty.openpty()
35
+ self.rows, self.columns = 45, 130
36
+ fcntl.ioctl(slave, termios.TIOCSWINSZ,
37
+ struct.pack('HHHH', self.rows, self.columns, 0, 0))
38
+ child_env = {**env, 'TERM': 'xterm-256color', 'COLORTERM': 'truecolor',
39
+ 'OPENCODE_PASSWORD': fixture['password']}
40
+ assert_clean_child_environment(child_env)
41
+ self.child = subprocess.Popen([binary, '--server', fixture['baseUrl'],
42
+ '--session', session_id], cwd=fixture['cwd'], env=child_env,
43
+ stdin=slave, stdout=slave, stderr=slave, start_new_session=True)
44
+ os.close(slave)
45
+ self.screen = Screen(rows=self.rows, columns=self.columns)
46
+ self.raw = bytearray()
47
+ self.stopped = False
48
+
49
+ def drain(self, seconds=.15):
50
+ end = time.monotonic() + seconds
51
+ while time.monotonic() < end:
52
+ if select.select([self.master], [], [], .04)[0]:
53
+ try: data = os.read(self.master, 65536)
54
+ except OSError: return
55
+ if not data: return
56
+ self.raw.extend(data)
57
+ self.screen.feed(data)
58
+ elif self.child.poll() is not None:
59
+ raise AssertionError(f'native TUI exited early: {self.child.returncode}')
60
+
61
+ def wait_for(self, predicate, description, timeout=12):
62
+ end = time.monotonic() + timeout
63
+ while time.monotonic() < end:
64
+ self.drain(.1)
65
+ if predicate():
66
+ self.drain(.12)
67
+ return
68
+ raise AssertionError(f'timed out waiting for {description}:\n{self.screen.visible()}')
69
+
70
+ def submit_slash(self, command, marker=None, select_next=False, extra_enter=False, use_tab=False):
71
+ # Native OpenCode opens slash completion on the first Enter; the second
72
+ # Enter accepts the exact typed command and invokes the plugin handler.
73
+ before = len(self.raw)
74
+ command_name, separator, arguments = command.partition(' ')
75
+ if select_next:
76
+ # Enter opens the duplicate command list; selecting the second exact
77
+ # /stats entry must dispatch the CTG handler rather than host stats.
78
+ os.write(self.master, command_name.encode('utf8') + b'\r')
79
+ self.drain(.35)
80
+ os.write(self.master, b'\x1b[B\x1b[B')
81
+ self.drain(.2)
82
+ os.write(self.master, b'\r')
83
+ else:
84
+ os.write(self.master, command_name.encode('utf8'))
85
+ self.drain(.35)
86
+ os.write(self.master, b'\t')
87
+ self.drain(.15)
88
+ if separator:
89
+ os.write(self.master, (' ' + arguments).encode('utf8'))
90
+ self.drain(.12)
91
+ os.write(self.master, b'\r')
92
+ if extra_enter:
93
+ self.drain(.12)
94
+ os.write(self.master, b'\r')
95
+ if marker:
96
+ self.wait_for(lambda: marker in self.screen.visible(), f'dispatched {command}')
97
+ return {'typed': command, 'ptyBytes': len(self.raw) - before,
98
+ 'visibleTail': self.screen.visible()[-500:]}
99
+
100
+ def send_prompt(self, text, marker):
101
+ os.write(self.master, text.encode('utf8') + b'\r')
102
+ self.wait_for(lambda: marker in self.screen.visible(), f'assistant marker {marker}')
103
+
104
+ def stop(self):
105
+ if self.stopped:
106
+ return
107
+ self.stopped = True
108
+ if self.child.poll() is None:
109
+ try: os.killpg(self.child.pid, signal.SIGTERM)
110
+ except ProcessLookupError: pass
111
+ try: self.child.wait(timeout=4)
112
+ except subprocess.TimeoutExpired:
113
+ os.killpg(self.child.pid, signal.SIGKILL)
114
+ self.child.wait(timeout=4)
115
+ try: os.close(self.master)
116
+ except OSError: pass
117
+
118
+
119
+ def main():
120
+ parser = argparse.ArgumentParser()
121
+ parser.add_argument('--binary', required=True)
122
+ parser.add_argument('--owned-root', required=True)
123
+ parser.add_argument('--release-manifest', required=True)
124
+ parser.add_argument('--fixture', required=True, help='JSON line emitted by native-fixture.mjs')
125
+ parser.add_argument('--artifact')
126
+ args = parser.parse_args()
127
+ fixture = json.loads(Path(args.fixture).read_text())
128
+ env = fixture_environment(os.environ, Path(fixture['temp']) / 'tui-home',
129
+ Path(args.owned_root).resolve(),
130
+ Path(args.release_manifest).resolve())
131
+ env = {key: value for key, value in env.items() if key in TUI_ENV_KEYS}
132
+ env['OPENCODE_SERVER_PASSWORD'] = fixture['password']
133
+ env['OPENCODE_SERVER_USERNAME'] = 'opencode'
134
+ assert_clean_child_environment(env)
135
+ binary = str(Path(args.binary).resolve())
136
+ digest = hashlib.sha256(Path(binary).read_bytes()).hexdigest()
137
+ tui = Tui(binary, fixture, fixture['sessionID'], env)
138
+ checks = {}
139
+ try:
140
+ tui.wait_for(lambda: '┃' in tui.screen.visible(), 'active session composer')
141
+ if 'getting started' in tui.screen.visible().lower():
142
+ for _ in range(2):
143
+ os.write(tui.master, b'\x1b')
144
+ tui.drain(.25)
145
+ tui.send_prompt('SLASH_FIXTURE_SEED <script>alert("x")</script> & "quoted"',
146
+ 'FIXTURE_REPLY')
147
+
148
+ export_name = 'slash-acceptance.html'
149
+ checks['exportDispatch'] = tui.submit_slash(f'/export {export_name}', 'Session exported')
150
+ exported = Path(fixture['cwd']) / export_name
151
+ end = time.monotonic() + 8
152
+ while not exported.exists() and time.monotonic() < end: time.sleep(.05)
153
+ html = exported.read_text(encoding='utf8')
154
+ assert '&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt; &amp; &quot;quoted&quot;' in html
155
+ assert '<script>alert("x")</script>' not in html
156
+ checks['exportHtmlEscapesFullConversation'] = {'path': export_name,
157
+ 'bytes': exported.stat().st_size, 'escapedPromptPresent': True,
158
+ 'activeScriptAbsent': True, 'completeDocument': html.startswith('<!doctype html>') and html.endswith('</html>')}
159
+
160
+ stats_start = len(tui.raw)
161
+ os.write(tui.master, b'/stats')
162
+ tui.drain(.3)
163
+ stats_result = tui.submit_slash('/stats details')
164
+ tui.drain(.2)
165
+ visible = tui.screen.visible()
166
+ stats_status = 'PASS' if 'Input tokens: not reported' in visible and 'Cost: not reported' in visible else 'FAIL'
167
+ stats_details = {'dialogRendered': stats_status == 'PASS',
168
+ 'observedNativeStatsInstead': 'Usage statistics' in visible,
169
+ 'rendered': visible[-1000:]}
170
+ if stats_status == 'PASS':
171
+ os.write(tui.master, b'\r')
172
+ tui.drain(.2)
173
+ else:
174
+ os.write(tui.master, b'\x1b')
175
+ tui.drain(.3)
176
+ stats_result = {'typed': '/stats details',
177
+ 'ptyBytes': len(tui.raw) - stats_start,
178
+ 'visibleTail': tui.screen.visible()[-500:]}
179
+ checks['statsDispatch'] = {**stats_result, 'status': stats_status}
180
+ checks['statsActualSessionAndUnknownHandling'] = stats_details
181
+
182
+ checks['guideTaskDispatch'] = tui.submit_slash('/guide framed task marker', 'GUIDE_FIXTURE_REPLY')
183
+ checks['guideToggleDispatch'] = tui.submit_slash('/guide ')
184
+ toggle_prompt = 'GUIDE_TOGGLE_SENTINEL exact text'
185
+ os.write(tui.master, b'\x15')
186
+ tui.drain(.1)
187
+ tui.send_prompt(toggle_prompt, 'FIXTURE_REPLY')
188
+ tui.drain(.3)
189
+ provider_log = Path(fixture['providerLog'])
190
+ provider_prompts = json.loads(provider_log.read_text()) if provider_log.exists() else []
191
+ toggle_provider_prompt = next((text for text in provider_prompts if toggle_prompt in text), '')
192
+ toggle_status = 'PASS' if toggle_provider_prompt == toggle_prompt else 'FAIL'
193
+ checks['guideToggleObserved'] = {'status': toggle_status,
194
+ 'providerPromptReceived': bool(toggle_provider_prompt),
195
+ 'framingDisabled': toggle_provider_prompt == toggle_prompt,
196
+ 'providerPromptExcerpt': toggle_provider_prompt[:300]}
197
+ checks['rawDispatch'] = tui.submit_slash('/raw RAW_SENTINEL exact text', 'RAW_FIXTURE_REPLY')
198
+ provider_prompts = json.loads(provider_log.read_text()) if provider_log.exists() else []
199
+ raw_prompt = next((text for text in provider_prompts if 'RAW_SENTINEL' in text), '')
200
+ checks['rawFramingBypass'] = {'status': 'PASS' if 'Deliver this task in phases' not in raw_prompt and raw_prompt.endswith('RAW_SENTINEL exact text') else 'FAIL',
201
+ 'providerPromptExcerpt': raw_prompt[:500]}
202
+ guide_prompt = next((text for text in provider_prompts if 'framed task marker' in text), '')
203
+ checks['guideTaskFraming'] = {'status': 'PASS' if 'Deliver this task in phases' in guide_prompt else 'FAIL',
204
+ 'providerPromptExcerpt': guide_prompt[:500]}
205
+
206
+ checks['workflowRoute'] = tui.submit_slash('/workflow', 'CARTHAGENT · WORKFLOW')
207
+ # The plugin route is persistent; subsequent slash text is not submitted
208
+ # from that custom renderer. Other views are exercised from independent
209
+ # sessions by the surrounding route-level harness, not claimed here.
210
+ tui.stop()
211
+ for command, expected, key in (
212
+ ('/architecture', 'CARTHAGENT · ARCHITECTURE', 'architectureRoute'),
213
+ ('/evidence', 'CARTHAGENT · EVIDENCE', 'evidenceRoute')):
214
+ route_tui = Tui(binary, fixture, fixture['sessionID'], env)
215
+ try:
216
+ route_tui.wait_for(lambda: '┃' in route_tui.screen.visible(), 'route session composer')
217
+ checks[key] = route_tui.submit_slash(command, expected)
218
+ finally:
219
+ route_tui.stop()
220
+ submitted = checks['submittedSlashes'] = [
221
+ '/export slash-acceptance.html', '/stats', '/guide framed task marker', '/guide ',
222
+ '/raw RAW_SENTINEL exact text', '/workflow', '/architecture', '/evidence']
223
+ assert len(submitted) == len(set(submitted)), 'slash submission evidence contains duplicate commands'
224
+ failures = [name for name, value in checks.items()
225
+ if isinstance(value, dict) and value.get('status') == 'FAIL']
226
+ result = {'status': 'PASS' if not failures else 'PARTIAL', 'failures': failures,
227
+ 'scope': 'Linux POSIX PTY; actual native plugin slash submissions; offline loopback provider',
228
+ 'binarySha256': digest, 'sessionID': fixture['sessionID'],
229
+ 'pluginReadiness': fixture['pluginReadiness'], 'providerRequests': fixture['providerRequests'],
230
+ 'checks': checks}
231
+ rendered = json.dumps(result, indent=2, ensure_ascii=False) + '\n'
232
+ print(rendered, end='')
233
+ if args.artifact:
234
+ Path(args.artifact).write_text(rendered, encoding='utf8')
235
+ if failures:
236
+ return 2
237
+ finally:
238
+ tui.stop()
239
+
240
+
241
+ if __name__ == '__main__':
242
+ main()
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /** Real OpenCode 2.0.32 offline SWE acceptance: advertised tools edit and verify a broken fixture. */
2
+ /** Real OpenCode 2.0.32 offline SWE acceptance through the composed production delivery workflow. */
3
3
  import assert from 'node:assert/strict';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
@@ -8,64 +8,130 @@ import { nativeFixture } from './native-fixture.mjs';
8
8
  import { runNativeEvaluation } from '../lib/opencode/evaluation.mjs';
9
9
 
10
10
  let runResult;
11
+ let reviewRequests = 0;
12
+ let reviewerSource = '';
13
+ let prePlanTools = [];
11
14
  const host = await nativeFixture({
12
- plugin: join(import.meta.dirname, '../lib/opencode/evaluation-budget-plugin'),
13
- plugins: process.argv[2] ? [process.argv[2]] : [join(import.meta.dirname, '../plugins/carthagent')],
14
- env: { CTG_EVALUATION_MAX_TURNS: '8', CTG_EVALUATION_MAX_COST: '1' },
15
+ plugin: process.argv[2] ?? join(import.meta.dirname, '../plugins/carthagent'),
16
+ pluginOptions: { reviewModel: { providerID: 'ctg-fixture', id: 'fixture' } },
17
+ plugins: [join(import.meta.dirname, '../lib/opencode/evaluation-budget-plugin')],
18
+ env: {
19
+ CTG_EVALUATION_MAX_TURNS: '30', CTG_EVALUATION_MAX_COST: '1',
20
+ CTG_SESSION_OPTIONS: JSON.stringify({ maxTools:60, maxSeconds:180, maxRepairs:20 }),
21
+ },
15
22
  respond(body, res) {
16
- const tools = body.tools || [];
17
- const edit = tools.find(tool => tool.function?.name === 'edit');
18
- const shell = tools.find(tool => tool.function?.name === 'shell');
19
- assert.ok(edit, 'OpenCode must advertise its actual edit tool schema to the local provider');
20
- assert.ok(shell, 'OpenCode must advertise its actual shell tool schema to the local provider');
23
+ const reviewText = JSON.stringify(body.messages ?? []);
21
24
  const root = host.cwd;
22
25
  const fixedFile = join(root, 'src/value.mjs');
23
- const testFile = join(root, 'src/value.test.mjs');
24
- const source = 'export const value = 42;\n';
25
- const testCommand = `cd ${JSON.stringify(root)} && ${JSON.stringify(process.execPath)} --test --test-reporter=tap ${JSON.stringify(testFile)}; code=$?; printf '\\nCTG_EXIT_CODE=%s\\n' "$code"; exit "$code"`;
26
- const messages = body.messages;
26
+ const messages = body.messages ?? [];
27
+ const tools = (body.tools ?? []).map(tool => tool.function?.name);
27
28
  const calls = [];
28
29
  const results = new Map();
29
30
  for (const message of messages) {
30
- for (const call of message.tool_calls || []) calls.push({ id:call.id, name:call.function?.name });
31
+ for (const call of message.tool_calls ?? []) calls.push({ id:call.id, name:call.function?.name, arguments:call.function?.arguments });
31
32
  if (message.role === 'tool' && message.tool_call_id) results.set(message.tool_call_id, String(message.content));
32
33
  }
33
34
  const completed = calls.filter(call => results.has(call.id)).map(call => ({ ...call, output:results.get(call.id) }));
34
- const editIndex = completed.findIndex(call => call.name === 'edit');
35
- const shellCalls = completed.filter(call => call.name === 'shell');
36
- const baseline = shellCalls.find(call => editIndex < 0 || completed.indexOf(call) < editIndex);
37
- const repair = editIndex >= 0 ? completed[editIndex] : undefined;
38
- const postEditChecks = repair ? shellCalls.filter(call => completed.indexOf(call) > editIndex) : [];
39
- const baselineObservedRed = Boolean(baseline && /(?:exit(?:ed)? with code\s*[:=]?\s*1|exit code\s*[:=]?\s*1|CTG_EXIT_CODE=1)/i.test(baseline.output)
40
- && /(?:not ok 1|ℹ\s*fail\s+1|#\s*fail\s+1)/i.test(baseline.output));
41
- const successfulCheck = postEditChecks.some(call => /(?:exit(?:ed)? with code\s*[:=]?\s*0|exit code\s*[:=]?\s*0|CTG_EXIT_CODE=0)/i.test(call.output)
42
- && /(?:ℹ\s*pass\s+1|#\s*pass\s+1)/i.test(call.output)
43
- && /(?:ℹ\s*fail\s+0|#\s*fail\s+0)/i.test(call.output));
44
- // A successful native edit tool result is completion evidence; unlike shell,
45
- // the edit tool does not emit an operating-system exit code.
46
- const editDone = Boolean(repair && !/\b(?:error|failed)\b/i.test(repair.output));
47
- const name = !baseline ? 'shell' : !repair ? 'edit' : !successfulCheck ? 'shell' : null;
48
- if (!name && !(baselineObservedRed && editDone && successfulCheck)) throw Error(`Native tool chronology did not establish red shell -> completed edit -> distinct green shell check: ${JSON.stringify(completed.map(({name,output})=>({name,output:output.slice(-1200)})))}`);
49
- const args = name === 'edit'
50
- ? { path: fixedFile, oldString: 'export const value = 0;\n', newString: source }
51
- : { command: testCommand, workdir: root, timeout: 15_000 };
52
- if (name === 'edit') assert.ok(baselineObservedRed, 'native shell tool must observe the initially failing test before editing');
53
- const toolCall = { index:0, id:`fixture-call-${host.requests.length}`, type:'function', function:{name,arguments:JSON.stringify(args)} };
54
- const delta = successfulCheck && editDone && baselineObservedRed
55
- ? { role:'assistant', content:'Fixed the implementation and confirmed the test passes.' }
56
- : { role:'assistant', content:null, tool_calls:[toolCall] };
57
- const finish = successfulCheck && editDone && baselineObservedRed ? 'stop' : 'tool_calls';
58
- res.writeHead(200, { 'content-type':'text/event-stream', 'cache-control':'no-cache', connection:'keep-alive' });
59
- const chunk = (choice, usage) => `data: ${JSON.stringify({id:`native-swe-${host.requests.length}`,object:'chat.completion.chunk',created:1,model:'fixture',choices:[choice],...(usage?{usage}:{})})}\n\n`;
60
- res.write(chunk({index:0,delta,finish_reason:null}));
61
- res.end(`${chunk({index:0,delta:{},finish_reason:finish},{prompt_tokens:20,completion_tokens:8,total_tokens:28})}data: [DONE]\n\n`);
35
+ const done = name => completed.filter(call => call.name === name);
36
+ const executeCalls = done('execute');
37
+ const codeOf = call => {
38
+ try { return JSON.parse(call.arguments ?? '{}').code ?? ''; }
39
+ catch { return String(call.arguments ?? ''); }
40
+ };
41
+ const executed = source => executeCalls.some(call => codeOf(call).includes(source));
42
+ const execution = (code) => dispatch('execute', { code });
43
+ if (messages.some(message => message.role === 'user' && String(message.content).includes('CTG_SWE_DELIVERY_FIXTURE')) && !executed('delivery_plan') && !prePlanTools.length) {
44
+ prePlanTools = tools;
45
+ }
46
+ if (reviewText.includes('Snapshot SHA-256:') && reviewText.includes('REVIEW_JSON:')) {
47
+ const snapshot = reviewText.match(/Snapshot SHA-256: ([a-f0-9]+)/)?.[1];
48
+ const head = reviewText.match(/HEAD: ([a-f0-9]+)/)?.[1];
49
+ reviewRequests++;
50
+ reviewerSource = reviewText;
51
+ if (!snapshot || !head) throw Error('Reviewer prompt omitted exact source identity');
52
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
53
+ const content = `Deterministic review receipt plumbing fixture.\nREVIEW_JSON: ${JSON.stringify({version:1,snapshot,head,findings:[]})}\nVERDICT: APPROVE`;
54
+ const chunk = (delta, finish_reason = null, usage) => `data: ${JSON.stringify({id:`native-review-${host.requests.length}`,object:'chat.completion.chunk',created:1,model:'fixture',choices:[{index:0,delta,finish_reason}],...(usage?{usage}:{})})}\n\n`;
55
+ res.end(`${chunk({role:'assistant',content})}${chunk({},'stop',{prompt_tokens:20,completion_tokens:8,total_tokens:28})}data: [DONE]\n\n`);
56
+ return;
57
+ }
58
+ if (JSON.stringify([...messages].reverse().find(message => message.role === 'user')).includes('STALE_FINISH_ATTEMPT')) {
59
+ return execution('const result = await tools.carthagent["delivery_finish"]({status:"verified",change:"Mutated fixture source after independent review.",review:"independent receipt is stale after source mutation",launch:"not applicable",limitations:[]}); return result');
60
+ }
61
+ const shell = () => dispatch('shell', {
62
+ command: 'node --test --test-reporter=tap src/value.test.mjs',
63
+ workdir: root,
64
+ });
65
+
66
+ if (!completed.length) {
67
+ assert.ok(tools.includes('execute'), 'production host must expose the Code Mode tool for discovery');
68
+ return execution('const result = await search({ query: "delivery plan acceptance checks review design tools", namespace: "carthagent", limit: 20 }); return result');
69
+ }
70
+ if (!executeCalls.some(call => codeOf(call).includes('delivery_finish'))) {
71
+ return execution('const result = await tools.carthagent["delivery_finish"]({status:"verified",change:"No implementation changes made.",review:"premature no-plan attempt",launch:"not applicable",limitations:[]}); return result');
72
+ }
73
+ if (!executed('delivery_finish')) throw Error('The no-plan verified finish call did not produce a tool response');
74
+ if (!executed('delivery_plan')) {
75
+ return execution(`const result = await tools.carthagent["delivery_plan"]({goal:"Repair fixture value so its test passes",assumptions:["The isolated Node test is the acceptance oracle."],artifacts:["src/value.mjs"],outputs:["src/value.mjs"],steps:[{id:"repair-value",title:"Repair the fixture value",checks:["fixture-test"]}],acceptance:[{requirement:"The exported value equals 42.",checks:["fixture-test"]}],checks:[{id:"fixture-test",kind:"test",argv:[${JSON.stringify(process.execPath)},"--test","src/value.test.mjs"],timeoutSeconds:20}]}); return {marker:"CTG_SWE_PLAN_ACCEPTED",result}`);
76
+ }
77
+ if (done('shell').length < 2) {
78
+ assert.ok(tools.includes('shell'), 'the valid standard delivery plan must unlock native shell');
79
+ return shell();
80
+ }
81
+ const baseline = done('shell')[1];
82
+ assert.match(baseline.output, /not ok 1|fail\s+1/i, 'native shell must observe red before any edit');
83
+ if (!done('edit').length) {
84
+ assert.ok(tools.includes('edit'), `production policy did not unlock the native edit tool after planning: ${tools.join(', ')}`);
85
+ return dispatch('edit', { path:fixedFile, oldString:'export const value = 0;\n', newString:'export const value = 42;\n' });
86
+ }
87
+ if (done('shell').length < 3) {
88
+ assert.ok(tools.includes('shell'), 'native shell must remain available for distinct post-edit verification');
89
+ return shell();
90
+ }
91
+ assert.match(done('shell')[2].output, /pass\s+1/i, 'post-edit native shell test must pass');
92
+ if (!executed('delivery_check')) {
93
+ return execution('return await tools.carthagent["delivery_check"]({id:"all"})');
94
+ }
95
+ if (!executeCalls.some(call => codeOf(call).includes('delivery_finish') && /premature attempt before progress/.test(codeOf(call)))) {
96
+ return execution('const result = await tools.carthagent["delivery_finish"]({status:"verified",change:"Changed fixture value from 0 to 42.",review:"premature attempt before progress",launch:"not applicable",limitations:[]}); return result');
97
+ }
98
+ if (!executed('delivery_progress')) {
99
+ return execution('const result = await tools.carthagent["delivery_progress"]({step:"repair-value",status:"done",note:"Native red/edit/green chronology completed."}); return {marker:"CTG_SWE_PROGRESS_DONE",result}');
100
+ }
101
+ const reviewCall = [...completed].reverse().find(call => call.name === 'execute'
102
+ && /delivery_review/.test(codeOf(call)) && /inspect/.test(codeOf(call)));
103
+ if (!reviewCall) return execution('return await tools.carthagent["delivery_review"]({action:"inspect"})');
104
+ const captureId = reviewCall.output.match(/"captureId"\s*:\s*"([^"]+)"/)?.[1]
105
+ ?? reviewCall.output.match(/"id"\s*:\s*"([0-9a-f-]{20,})"/)?.[1];
106
+ if (!captureId) throw Error(`Review inspect result did not expose capture ID: ${reviewCall.output}`);
107
+ if (!executeCalls.some(call => /delivery_review/.test(codeOf(call)) && /record/.test(codeOf(call)))) {
108
+ const coverage = [{ requirement:'The exported value equals 42.', assertions:'The fresh fixture-test runs node --test against src/value.test.mjs and asserts value === 42.', status:'demonstrated' }];
109
+ const probes = ['The original value 0 fails the assertion before editing.', 'After editing, the separate native shell invocation passes the same assertion.'];
110
+ return execution(`const result = await tools.carthagent["delivery_review"]({action:"record",captureId:${JSON.stringify(captureId)},coverage:${JSON.stringify(coverage)},probes:${JSON.stringify(probes)},findings:[],limitations:["Offline host proof uses model-authored workflow review; no independent reviewer service is configured.","This fixture verifies native workflow integration, not delivery-policy enforcement."]}); return {marker:"CTG_SWE_REVIEW_RECORDED",result}`);
111
+ }
112
+ if (done('execute').filter(call => codeOf(call).includes('delivery_finish') && /Fresh declared test/.test(codeOf(call))).length < 1) {
113
+ return execution('const result = await tools.carthagent["delivery_finish"]({status:"verified",change:"Changed fixture value from 0 to 42 to satisfy the existing assertion.",review:"Fresh declared test passed and model-authored coverage/probes were recorded; independent review was not configured.",launch:"Not applicable to this isolated module fixture.",limitations:["No independent reviewer service was configured; this is not independent review assurance.","This fixture acceptance does not claim delivery-policy enforcement."]}); return {marker:"CTG_SWE_FINISHED",result}');
114
+ }
115
+ return respondText('The fixture fix passed the declared check and the composed delivery workflow accepted its honest, limited handoff.');
116
+
117
+ function dispatch(name, args) {
118
+ if (!tools.includes(name)) throw Error(`Production delivery policy is withholding required native tool ${name}; advertised: ${tools.join(', ')}`);
119
+ const call = { index:0, id:`fixture-call-${host.requests.length}`, type:'function', function:{name,arguments:JSON.stringify(args)} };
120
+ res.writeHead(200, { 'content-type':'text/event-stream', 'cache-control':'no-cache', connection:'keep-alive' });
121
+ const chunk = (delta, finish_reason = null, usage) => `data: ${JSON.stringify({id:`native-swe-${host.requests.length}`,object:'chat.completion.chunk',created:1,model:'fixture',choices:[{index:0,delta,finish_reason}],...(usage?{usage}:{})})}\n\n`;
122
+ res.end(`${chunk({role:'assistant',tool_calls:[call]})}${chunk({},'tool_calls',{prompt_tokens:20,completion_tokens:8,total_tokens:28})}data: [DONE]\n\n`);
123
+ return call.id;
124
+ }
125
+ function respondText(text) {
126
+ res.writeHead(200, { 'content-type':'text/event-stream' });
127
+ const chunk = (delta, finish_reason = null, usage) => `data: ${JSON.stringify({id:`native-swe-${host.requests.length}`,object:'chat.completion.chunk',created:1,model:'fixture',choices:[{index:0,delta,finish_reason}],...(usage?{usage}:{})})}\n\n`;
128
+ res.end(`${chunk({role:'assistant',content:text})}${chunk({},'stop',{prompt_tokens:20,completion_tokens:8,total_tokens:28})}data: [DONE]\n\n`);
129
+ }
62
130
  },
63
131
  });
64
132
 
65
133
  try {
66
134
  const { client, cwd: directory, location, model, request } = host;
67
- await host.active('ctg.evaluation-budget', location);
68
- await host.active('carthagent', location);
69
135
  await mkdir(join(directory, 'src'), { recursive: true });
70
136
  await writeFile(join(directory, 'src/value.mjs'), 'export const value = 0;\n');
71
137
  await writeFile(join(directory, 'src/value.test.mjs'), [
@@ -75,19 +141,36 @@ try {
75
141
  "test('fixture value is repaired', () => assert.equal(value, 42));",
76
142
  '',
77
143
  ].join('\n'));
144
+ const git = (...args) => {
145
+ const result = spawnSync('git', args, { cwd: directory, encoding: 'utf8' });
146
+ assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stderr}`);
147
+ return result.stdout.trim();
148
+ };
149
+ git('init', '--initial-branch=main');
150
+ git('add', 'src/value.mjs', 'src/value.test.mjs');
151
+ git('-c', 'user.name=CTG fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-m', 'SWE fixture baseline');
152
+ await host.active('ctg.evaluation-budget', location);
153
+ await host.active('carthagent', location);
154
+ await assert.rejects(readFile(join(directory, 'unplanned.txt')), 'no unplanned shell execution was issued or created its requested file');
78
155
  const baseline = spawnSync(process.execPath, ['--test', '--test-reporter=tap', join(directory, 'src/value.test.mjs')], { cwd: directory, encoding: 'utf8' });
79
156
  assert.notEqual(baseline.status, 0, `fixture acceptance starts red: ${baseline.stdout}${baseline.stderr}`);
80
157
 
81
158
  const result = runResult = await runNativeEvaluation({
82
159
  client, directory,
83
- prompt: `Use the advertised edit tool to fix src/value.mjs so the test passes, then use the advertised shell tool to run ${JSON.stringify(process.execPath)} --test src/value.test.mjs. Do not claim success unless the test output confirms it.\nFIXTURE_ROOT=${directory}`,
84
- provider: model.providerID, model: model.id, timeoutSeconds: 60, maxTurns: 8, maxCost: 1,
160
+ agent: 'ship',
161
+ metadata: { 'carthagent.options': { maxTools:60, maxSeconds:180, maxRepairs:20 } },
162
+ prompt: `CTG_SWE_DELIVERY_FIXTURE. Discover the registered CTG tools with search; author and follow a valid required-verification delivery plan with a declared test check. Respect standard production tool policy: observe the failing baseline, make the native edit only after the plan unlocks it, run a distinct passing native shell check, record progress, run the declared check, inspect and record honest review coverage/probes, and finish with limitations. Do not claim independent review or delivery-policy enforcement.\nFIXTURE_ROOT=${directory}`,
163
+ provider: model.providerID, model: model.id, timeoutSeconds: 180, maxTurns: 30, maxCost: 1,
85
164
  });
86
165
  assert.equal(result.runtime, 'opencode-2');
87
- assert.equal(result.opencodeVersion, '2.0.32');
166
+ const hostInfo = await client.server.info(request());
167
+ assert.equal(result.compatibilityVersion, '2.0.32');
168
+ assert.equal(result.runtimeVersion, hostInfo.version, 'evaluator reports the actual connected server version');
169
+ assert.equal(result.opencodeVersion, hostInfo.version, 'legacy version field retains actual-server semantics');
88
170
  assert.equal(result.exitCode, 0, JSON.stringify(result));
89
171
  assert.equal(result.outcome, 'succeeded');
90
- assert.ok(result.productiveTurns >= 4, JSON.stringify(result));
172
+ assert.equal(prePlanTools.includes('shell'), false, 'native host withholds shell before a valid standard plan');
173
+ assert.ok(result.productiveTurns >= 8, JSON.stringify(result));
91
174
  assert.ok(result.tools.edit >= 1, JSON.stringify(result));
92
175
  assert.ok(result.tools.shell >= 2, JSON.stringify(result));
93
176
  assert.ok(Number.isFinite(result.cost) && result.cost >= 0, JSON.stringify(result));
@@ -96,33 +179,119 @@ try {
96
179
  assert.match(final.stdout, /(?:# pass 1|ℹ pass 1)/);
97
180
  assert.equal(await readFile(join(directory, 'src/value.mjs'), 'utf8'), 'export const value = 42;\n');
98
181
  const aggregate = await client.session.get({ sessionID: result.sessionID }, request());
182
+ assert.equal(aggregate.agent, 'ship', 'delivery evaluation must explicitly select Ship');
183
+ assert.deepEqual(aggregate.metadata['carthagent.options'], { maxTools:60, maxSeconds:180, maxRepairs:20 }, 'caller delivery options must survive evaluation admission');
184
+ assert.deepEqual(aggregate.metadata['ctg.evaluation.budget'], { maxTurns:30, maxCost:1 }, 'evaluation admission retains its authoritative budget');
99
185
  assert.equal(result.cost, aggregate.cost);
100
186
  assert.ok(result.tokens > 0);
101
187
  const timeline = [];
102
188
  const knownCalls = new Map();
189
+ const workflowTools = new Set();
103
190
  for (const request of host.requests) for (const message of request.messages) {
104
191
  for (const call of message.tool_calls || []) {
105
192
  knownCalls.set(call.id, call.function?.name);
106
- timeline.push({ kind:'call', id:call.id, name:call.function?.name });
193
+ let args = {};
194
+ try { args = JSON.parse(call.function?.arguments ?? '{}'); } catch {}
195
+ if (call.function?.name === 'execute') {
196
+ const code = String(args.code ?? '');
197
+ for (const name of ['delivery_plan','delivery_check','delivery_progress','delivery_review','delivery_finish']) if (code.includes(name)) workflowTools.add(name);
198
+ }
199
+ timeline.push({ kind:'call', id:call.id, name:call.function?.name, arguments:args });
107
200
  }
108
201
  if (message.role === 'tool' && knownCalls.has(message.tool_call_id)) {
109
202
  timeline.push({ kind:'result', id:message.tool_call_id, name:knownCalls.get(message.tool_call_id), output:String(message.content) });
110
203
  }
111
204
  }
112
- const baselineAt = timeline.findIndex(item => item.kind === 'result' && item.name === 'shell'
113
- && /(?:exit(?:ed)? with code\s*[:=]?\s*1|exit code\s*[:=]?\s*1|CTG_EXIT_CODE=1)/i.test(item.output) && /(?:not ok 1|ℹ\s*fail\s+1|#\s*fail\s+1)/i.test(item.output));
205
+ const planAt = timeline.findIndex(item => item.kind === 'result' && item.name === 'execute' && /CTG_SWE_PLAN_ACCEPTED|Contract accepted|plan\.d2/.test(item.output));
206
+ const baselineAt = timeline.findIndex((item, index) => index > planAt && item.kind === 'result' && item.name === 'shell'
207
+ && /(?:exit(?:ed)? with code\s*[:=]?\s*1|exit code\s*[:=]?\s*1)/i.test(item.output) && /(?:not ok 1|ℹ\s*fail\s+1|#\s*fail\s+1)/i.test(item.output));
114
208
  const editAt = timeline.findIndex((item,index) => index > baselineAt && item.kind === 'result' && item.name === 'edit' && !/\b(?:error|failed)\b/i.test(item.output));
115
209
  const greenAt = timeline.findIndex((item,index) => index > editAt && item.kind === 'result' && item.name === 'shell'
116
- && /(?:exit(?:ed)? with code\s*[:=]?\s*0|exit code\s*[:=]?\s*0|CTG_EXIT_CODE=0)/i.test(item.output)
117
- && /(?:ℹ\s*pass\s+1|#\s*pass\s+1)/i.test(item.output)
210
+ && !/(?:exit(?:ed)? with code\s*[:=]?\s*1|exit code\s*[:=]?\s*1)/i.test(item.output)
211
+ && /(?:#\s*pass\s+1|ℹ\s*pass\s+1)/i.test(item.output)
118
212
  && /(?:ℹ\s*fail\s+0|#\s*fail\s+0)/i.test(item.output));
119
213
  assert.ok(baselineAt >= 0, 'recorded native shell result must prove baseline red');
120
214
  assert.ok(editAt > baselineAt, 'completed native edit must follow the baseline failure');
121
215
  assert.ok(greenAt > editAt, 'a distinct completed native shell call must prove the post-edit passing test');
216
+ for (const tool of ['delivery_plan', 'delivery_check', 'delivery_progress', 'delivery_review', 'delivery_finish']) {
217
+ assert.ok(workflowTools.has(tool), `composed production workflow must execute ${tool}`);
218
+ }
219
+ const finishCalls = timeline.filter(item => item.kind === 'call' && item.name === 'execute' && String(item.arguments.code ?? '').includes('delivery_finish'));
220
+ assert.ok(finishCalls.length >= 3, 'fixture must issue no-plan, pre-progress, and final finish attempts');
221
+ const noPlanFinish = timeline.find(item => item.kind === 'result' && item.id === finishCalls[0].id);
222
+ assert.match(noPlanFinish?.output ?? '', /Call delivery_plan first/);
223
+ const beforeProgress = finishCalls.find(call => /premature attempt before progress/.test(String(call.arguments.code ?? '')));
224
+ assert.ok(beforeProgress);
225
+ const beforeProgressResult = timeline.find(item => item.kind === 'result' && item.id === beforeProgress.id)?.output ?? '';
226
+ assert.match(beforeProgressResult, /incompleteSteps/);
227
+ assert.match(beforeProgressResult, /repair-value/);
228
+ const finishCall = finishCalls.at(-1);
229
+ assert.ok(finishCall, 'native workflow must issue delivery_finish');
230
+ const finishResult = timeline.find(item => item.kind === 'result' && item.id === finishCall.id);
231
+ assert.match(finishResult?.output ?? '', /\\"status\\"\s*:\s*\\"verified\\"/, 'production delivery gate must accept the completed declared check and review receipt');
232
+ assert.ok(timeline.some(item => item.kind === 'call' && item.name === 'execute' && /not independent review|independent review was not configured/i.test(String(item.arguments.code ?? ''))), 'fixture must accurately disclose model-authored review limitation');
233
+ const workspaceRpc = (method, input) => client.rpc.call({ rpcID: 'carthagent.workspace', method, input, location }, request());
234
+ let independentResponse;
235
+ const reviewTarget = await client.session.get({ sessionID: result.sessionID }, request());
236
+ try { independentResponse = await workspaceRpc('startReview', { sessionID: reviewTarget.id }); }
237
+ catch (error) { console.error(JSON.stringify({ error, providerErrors:host.providerErrors.map(item=>item.message), host:host.output }, null, 2)); throw error; }
238
+ const independent = independentResponse.output;
239
+ assert.equal(independent.status, 'approved', JSON.stringify(independent));
240
+ assert.ok(independent.sessionID && independent.sessionID !== result.sessionID, 'reviewer must be a distinct session');
241
+ const reviewer = await client.session.get({ sessionID: independent.sessionID }, request());
242
+ assert.equal(reviewer.parentID, undefined, 'reviewer must be a fresh root session');
243
+ assert.equal(reviewer.location.directory, directory, 'reviewer must inspect this exact delivery worktree');
244
+ assert.equal(reviewRequests, 1, 'independent reviewer must actually be invoked');
245
+ assert.match(reviewerSource, /export const value = 42/, 'reviewer source snapshot must contain the actual delivered edit');
246
+ assert.ok(Array.isArray(reviewer.permissions), 'reviewer session must persist its explicit read-only permission policy');
247
+ assert.ok(reviewer.permissions.some(rule => rule.action === '*' && rule.effect === 'deny'));
248
+ assert.ok(reviewer.permissions.some(rule => rule.action === 'read' && rule.effect === 'allow'));
249
+ for (const action of ['shell', 'edit', 'write', 'execute', 'subagent']) {
250
+ assert.ok(reviewer.permissions.some(rule => rule.action === action && rule.effect === 'deny'), `reviewer permission policy must deny ${action}`);
251
+ }
252
+ const reviewMessages = await client.session.context({sessionID:independent.sessionID},request());
253
+ assert.ok(reviewMessages.some(message => message.type === 'assistant' && message.content?.some(part => part.type === 'text' && /REVIEW_JSON:/.test(part.text))), 'fresh reviewer session must complete a structured review response');
254
+ assert.equal((await workspaceRpc('review', { sessionID: reviewTarget.id })).output.status, 'approved');
255
+ await writeFile(join(directory, 'src/value.mjs'), 'export const value = 43;\n');
256
+ assert.equal((await workspaceRpc('review', { sessionID: reviewTarget.id })).output.status, 'stale', 'source edits must invalidate the exact-worktree review receipt');
257
+ const altered = spawnSync(process.execPath, ['--test', '--test-reporter=tap', join(directory, 'src/value.test.mjs')], { cwd: directory, encoding: 'utf8' });
258
+ assert.notEqual(altered.status, 0, 'post-review source mutation must break the declared acceptance test');
259
+ assert.equal((await workspaceRpc('startReview', { sessionID: reviewTarget.id })).output.status, 'approved');
260
+ await writeFile(join(directory, 'src/value.mjs'), 'export const value = 44;\n');
261
+ assert.equal((await workspaceRpc('startReview', { sessionID: reviewTarget.id })).output.status, 'approved');
262
+ await writeFile(join(directory, 'src/value.mjs'), 'export const value = 45;\n');
263
+ const beforeAlteredFinish = reviewRequests;
264
+ await assert.rejects(workspaceRpc('startReview', { sessionID: reviewTarget.id }));
265
+ assert.equal(reviewRequests, beforeAlteredFinish, 'review admission must stop after its configured round cap');
266
+ await client.session.prompt({sessionID:result.sessionID,text:'STALE_FINISH_ATTEMPT: try to finish verified using now stale evidence.'},request());
267
+ await client.session.wait({sessionID:result.sessionID},{signal:AbortSignal.timeout(45_000)});
268
+ const staleFinishPrompt = [...host.requests].reverse().find(request => request.messages?.some(message => message.role === 'user' && String(message.content).includes('STALE_FINISH_ATTEMPT')))
269
+ ?? host.requests.at(-1);
270
+ assert.ok(staleFinishPrompt, 'stale-finish probe must have its own model request');
271
+ const staleFinishCall = [...staleFinishPrompt.messages.flatMap(message => message.tool_calls ?? [])].reverse().find(call => {
272
+ if (call.function?.name !== 'execute') return false;
273
+ try { return JSON.parse(call.function.arguments ?? '{}').code?.includes('independent receipt is stale after source mutation'); }
274
+ catch { return false; }
275
+ });
276
+ assert.ok(staleFinishCall, 'stale-finish turn must issue its own verified-finish call');
277
+ const staleFinishResult = staleFinishPrompt.messages.find(message => message.role === 'tool' && message.tool_call_id === staleFinishCall.id);
278
+ assert.ok(staleFinishResult, `stale-finish tool call ${staleFinishCall.id} must have a correlated result`);
279
+ assert.match(String(staleFinishResult.content), /Cannot verify:/, 'the source-mutated finish call itself must be rejected');
280
+ assert.match(String(staleFinishResult.content), /pendingChecks/, 'source mutation must invalidate declared check evidence');
281
+ assert.doesNotMatch(String(staleFinishResult.content), /"status"\s*:\s*"verified"/);
122
282
  console.log(JSON.stringify({
123
- host: '2.0.32', outcome: result.outcome, productiveTurns: result.productiveTurns,
283
+ host: { compatibilityVersion: result.compatibilityVersion, runtimeVersion: result.runtimeVersion }, outcome: result.outcome, productiveTurns: result.productiveTurns,
124
284
  tools: result.tools, cost: result.cost, baseline: 'native shell failed', nativePostEditCheck: 'native shell passed', finalTest: 'passed',
125
- realOpenCodeHost: true, loopbackProvider: true,
285
+ realOpenCodeHost: true, loopbackProvider: true, composedProductionDelivery: true,
286
+ policyEvidence: {
287
+ unplannedShell: 'withheld before plan; no policy-hook rejection claim',
288
+ plannedNativeShellPositiveControl: 'passed',
289
+ verifiedFinishWithoutPlan: 'rejected',
290
+ verifiedFinishBeforeProgress: 'rejected',
291
+ verifiedFinishAfterSourceMutation: 'rejected because declared evidence is stale',
292
+ },
293
+ review: 'same-worktree root reviewer configured with persisted read-only permissions; source-bound receipt invalidated by later edit; deterministic verdict proves receipt plumbing only, not review quality',
294
+ finalTestPassedBeforeMutation: true, alteredSourceTestFailedIntentionally: true,
126
295
  }));
127
296
  } finally {
128
297
  if (runResult?.exitCode) {