carthagent 1.0.7 → 1.1.0-preview.13

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 (289) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  4. package/docs/glyph-review/test-isolation-v1.md +26 -0
  5. package/docs/glyph-review/typecheck-base.log +1 -0
  6. package/docs/glyph-review/typecheck-v4.log +1 -0
  7. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  8. package/docs/help-dispatch-review.md +69 -0
  9. package/docs/legacy-cli.md +150 -0
  10. package/docs/live-theme-plugin-api.md +18 -0
  11. package/docs/native-cli.md +102 -0
  12. package/docs/native-compatibility.md +16 -0
  13. package/docs/native-daily-quests.md +132 -0
  14. package/docs/native-glyph-pty.md +59 -0
  15. package/docs/native-manual-testing.md +336 -0
  16. package/docs/native-slash-acceptance-handoff.md +49 -0
  17. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  18. package/docs/native-terminal-performance.md +310 -0
  19. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  20. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  21. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  22. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  23. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  24. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  31. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  32. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  33. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  35. package/docs/native-terminal-semantics.md +89 -0
  36. package/docs/native-upgrade.md +177 -0
  37. package/docs/opencode-composed-source-review.md +89 -0
  38. package/docs/opencode-migration.md +225 -9
  39. package/docs/opencode-owned-runtime.md +241 -0
  40. package/docs/opencode-runtime.md +12 -0
  41. package/docs/opencode-theme-provider-proof/README.md +55 -0
  42. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  43. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  44. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  45. package/docs/owned-host-controls-proof.md +39 -0
  46. package/docs/owned-runtime-release-manifest.md +42 -0
  47. package/docs/owned-visual-clear-proof.md +40 -0
  48. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  49. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  50. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  51. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  52. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  53. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  54. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  58. package/docs/release-notes-native-draft.md +73 -0
  59. package/docs/release-notes-preview.10.md +121 -0
  60. package/docs/release-notes-preview.11.md +76 -0
  61. package/docs/release-notes-preview.12.md +55 -0
  62. package/docs/release-notes-preview.13.md +68 -0
  63. package/docs/release-notes-preview.9.md +130 -0
  64. package/docs/source-preview.md +120 -0
  65. package/evaluate.mjs +28 -1
  66. package/lib/budget.mjs +1 -1
  67. package/lib/codegraph.mjs +1 -1
  68. package/lib/delivery-review.mjs +11 -1
  69. package/lib/delivery.mjs +3 -2
  70. package/lib/environment.mjs +8 -0
  71. package/lib/guidance.mjs +1 -1
  72. package/lib/opencode/cli-runtime.mjs +1171 -0
  73. package/lib/opencode/cli.mjs +252 -0
  74. package/lib/opencode/credentials.mjs +63 -8
  75. package/lib/opencode/demo.mjs +53 -0
  76. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  77. package/lib/opencode/evaluation.mjs +32 -12
  78. package/lib/opencode/headless.mjs +8 -4
  79. package/lib/opencode/provider-policy.mjs +23 -0
  80. package/lib/opencode/review.mjs +37 -16
  81. package/lib/opencode/runtime-artifact.mjs +335 -0
  82. package/lib/opencode/runtime-github-transport.mjs +121 -0
  83. package/lib/opencode/runtime-release.json +36 -0
  84. package/lib/opencode/runtime.mjs +299 -20
  85. package/lib/opencode/service.mjs +77 -35
  86. package/lib/opencode/session-options.mjs +6 -2
  87. package/lib/opencode/update.mjs +222 -0
  88. package/lib/opencode/workspace.mjs +1 -1
  89. package/lib/planning-advice.mjs +2 -2
  90. package/lib/providers/experiential.mjs +2 -2
  91. package/lib/tui/framing.mjs +1 -80
  92. package/lib/update.mjs +10 -1
  93. package/package.json +33 -7
  94. package/plugins/carthagent/README.md +23 -0
  95. package/plugins/carthagent/src/bootstrap.ts +1 -1
  96. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  97. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  98. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  99. package/plugins/carthagent/src/index.ts +71 -24
  100. package/plugins/carthagent/src/rpc.ts +51 -6
  101. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  102. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  103. package/plugins/carthagent/src/server/daily-source-rpc.ts +211 -0
  104. package/plugins/carthagent/src/server/delivery.ts +227 -112
  105. package/plugins/carthagent/src/server/framing.mjs +80 -0
  106. package/plugins/carthagent/src/server/policy.ts +1 -1
  107. package/plugins/carthagent/src/server/providers.ts +59 -31
  108. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  109. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  110. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  111. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  112. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  113. package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
  114. package/plugins/carthagent/src/testing.ts +5 -0
  115. package/plugins/carthagent/src/tui.tsx +496 -0
  116. package/plugins/carthagent/src/ui/client.ts +125 -0
  117. package/plugins/carthagent/src/ui/commands.ts +70 -0
  118. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  119. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  120. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  121. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  122. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  123. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  124. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  125. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  126. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  127. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  128. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  129. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  130. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  131. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  132. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  133. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  134. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  135. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  136. package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
  137. package/plugins/carthagent/src/ui/voyages.tsx +129 -0
  138. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  139. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  140. package/plugins/carthagent/src/voyages-rpc.ts +60 -0
  141. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  142. package/plugins/carthagent/tests/commands.test.ts +107 -0
  143. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  144. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  145. package/plugins/carthagent/themes/carthage.json +67 -0
  146. package/plugins/carthagent/tui.ts +26 -1
  147. package/script/ctg/build-owned-runtime.sh +166 -0
  148. package/tests/budget-engine.test.mjs +4 -3
  149. package/tests/cli.test.mjs +52 -96
  150. package/tests/daily-evidence.test.mjs +62 -0
  151. package/tests/delivery-review.test.mjs +11 -0
  152. package/tests/delivery.test.mjs +22 -1
  153. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  154. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  155. package/tests/installed-runtime-contract.test.mjs +43 -0
  156. package/tests/native-cli-host-smoke.mjs +457 -0
  157. package/tests/native-daily-editor-focus.test.mjs +213 -0
  158. package/tests/native-daily-editor.test.mjs +43 -0
  159. package/tests/native-daily-quests-layout.test.mjs +53 -0
  160. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  161. package/tests/native-daily-quests-ui.test.mjs +778 -0
  162. package/tests/native-daily-source.test.mjs +451 -0
  163. package/tests/native-demo-ui.test.mjs +48 -0
  164. package/tests/native-evaluation-admission.test.mjs +24 -0
  165. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  166. package/tests/native-fixture-runtime.test.mjs +111 -0
  167. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  168. package/tests/native-glyph-pty.test.mjs +107 -0
  169. package/tests/native-manual-projects.test.mjs +170 -0
  170. package/tests/native-model-command.test.mjs +67 -0
  171. package/tests/native-owned-runtime-env.test.mjs +140 -0
  172. package/tests/native-paired-guards.test.mjs +464 -0
  173. package/tests/native-provider-policy.test.mjs +31 -0
  174. package/tests/native-pty-invocation.test.mjs +114 -0
  175. package/tests/native-pty-runtime.test.mjs +45 -0
  176. package/tests/native-resume-session.test.mjs +46 -0
  177. package/tests/native-review-consumer.test.mjs +286 -0
  178. package/tests/native-runtime-identity.test.mjs +214 -0
  179. package/tests/native-select-mouse.test.mjs +23 -0
  180. package/tests/native-ship-parity.test.mjs +526 -0
  181. package/tests/native-ship-plan.test.mjs +85 -0
  182. package/tests/native-ship-rpc.test.mjs +71 -0
  183. package/tests/native-ui-commands.test.mjs +6 -0
  184. package/tests/native-voyages-layout.test.mjs +28 -0
  185. package/tests/native-voyages-navigation.test.mjs +169 -0
  186. package/tests/native-voyages-rpc.test.mjs +395 -0
  187. package/tests/native-voyages-ui.test.mjs +277 -0
  188. package/tests/opencode-atomic-write.test.mjs +55 -0
  189. package/tests/opencode-cli.test.mjs +1312 -0
  190. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  191. package/tests/opencode-delivery.test.mjs +16 -0
  192. package/tests/opencode-providers.test.mjs +245 -18
  193. package/tests/opencode-runtime.test.mjs +546 -8
  194. package/tests/opencode-session-controls.test.mjs +15 -0
  195. package/tests/opencode-ship-rpc.test.mjs +71 -0
  196. package/tests/opencode-update.test.mjs +475 -0
  197. package/tests/opencode-workspace.test.mjs +51 -2
  198. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  199. package/tests/release-workflow.test.mjs +46 -0
  200. package/tests/runtime-artifact.test.mjs +269 -0
  201. package/tests/runtime-development-trust.test.mjs +230 -0
  202. package/tests/runtime-github-transport.test.mjs +132 -0
  203. package/tests/runtime-token-env.test.mjs +23 -0
  204. package/tests/server.test.mjs +3 -1
  205. package/tests/ship-telemetry.test.mjs +1 -1
  206. package/tests/source-preview.test.mjs +253 -0
  207. package/tests/test_native_terminal_probe.py +76 -0
  208. package/tests/test_native_terminal_semantics.py +232 -0
  209. package/tests/update.test.mjs +42 -5
  210. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  211. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  212. package/tests/workspace-live.test.mjs +127 -0
  213. package/tools/benchmark/diagnose-request.mjs +59 -36
  214. package/tools/benchmark/eval-common.mjs +3 -3
  215. package/tools/benchmark/evaluators.test.mjs +4 -4
  216. package/tools/benchmark/guard.test.mjs +35 -2
  217. package/tools/benchmark/guard.ts +20 -7
  218. package/tools/benchmark/planning-startup.test.mjs +30 -34
  219. package/tools/benchmark/runner.mjs +12 -7
  220. package/tools/benchmark/runner.test.mjs +23 -7
  221. package/tools/benchmark/upstream-error.test.mjs +16 -2
  222. package/tools/generate-owned-runtime-release.mjs +217 -0
  223. package/tools/installed-runtime-contract.mjs +16 -0
  224. package/tools/manual-runtime-manifest.mjs +39 -0
  225. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  226. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  227. package/tools/native-fixture.mjs +199 -40
  228. package/tools/native-glyph-helpers.py +127 -0
  229. package/tools/native-glyph-pty.py +144 -0
  230. package/tools/native-manual-projects.mjs +259 -0
  231. package/tools/native-manual.mjs +338 -0
  232. package/tools/native-model-picker-acceptance.py +306 -0
  233. package/tools/native-owned-runtime-env.sh +47 -0
  234. package/tools/native-paired-guards.mjs +282 -0
  235. package/tools/native-paired-manifest.mjs +72 -0
  236. package/tools/native-paired-orchestration.sh +51 -0
  237. package/tools/native-plugin-readiness.mjs +33 -0
  238. package/tools/native-plugin-readiness.test.mjs +53 -0
  239. package/tools/native-pty-invocation.mjs +26 -0
  240. package/tools/native-pty-runtime.mjs +83 -0
  241. package/tools/native-pty-smoke.py +147 -24
  242. package/tools/native-public-repo-shell-result.mjs +13 -0
  243. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  244. package/tools/native-runtime-identity.mjs +201 -0
  245. package/tools/native-slash-acceptance-env.py +38 -0
  246. package/tools/native-terminal-semantics.py +876 -0
  247. package/tools/owned-shell-observer/index.ts +16 -0
  248. package/tools/owned-shell-observer/package.json +1 -0
  249. package/tools/owned-visual-clear-env.py +48 -0
  250. package/tools/owned-visual-clear-model.py +21 -0
  251. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  252. package/tools/prepare-native-manual.mjs +122 -0
  253. package/tools/run-native-terminal-paired.sh +403 -0
  254. package/tools/runtime-token-env.mjs +8 -0
  255. package/tools/seed-native-switch-fixture.mjs +79 -0
  256. package/tools/test-native-controls.mjs +3 -1
  257. package/tools/test-native-daily-quests.mjs +65 -0
  258. package/tools/test-native-default-cli.mjs +106 -0
  259. package/tools/test-native-delivery-host.mjs +102 -20
  260. package/tools/test-native-evaluation.mjs +7 -3
  261. package/tools/test-native-glyph-pty.mjs +94 -0
  262. package/tools/test-native-host.mjs +87 -63
  263. package/tools/test-native-outcomes.mjs +3 -1
  264. package/tools/test-native-package.mjs +206 -38
  265. package/tools/test-native-performance.mjs +26 -0
  266. package/tools/test-native-product.mjs +90 -0
  267. package/tools/test-native-pty.mjs +34 -9
  268. package/tools/test-native-public-repo.mjs +224 -0
  269. package/tools/test-native-refresh-reentry.mjs +106 -0
  270. package/tools/test-native-report.mjs +27 -20
  271. package/tools/test-native-review.mjs +75 -0
  272. package/tools/test-native-runtime.mjs +120 -0
  273. package/tools/test-native-ship-models.mjs +176 -0
  274. package/tools/test-native-ship-walkthrough.mjs +96 -0
  275. package/tools/test-native-slash-commands.mjs +125 -0
  276. package/tools/test-native-slash-commands.py +242 -0
  277. package/tools/test-native-swe-host.mjs +224 -55
  278. package/tools/test-native-tasks.mjs +77 -0
  279. package/tools/test-native-terminal-performance.py +887 -0
  280. package/tools/test-native-terminal-readiness.sh +85 -0
  281. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  282. package/tools/test-native-visual-clear-installed.mjs +48 -0
  283. package/tools/test-owned-host-controls.mjs +238 -0
  284. package/tools/test-owned-opencode-runtime.mjs +35 -0
  285. package/tools/test-owned-visual-clear-env.py +120 -0
  286. package/tools/test-owned-visual-clear.py +373 -0
  287. package/tools/verify-draft-runtime-gate.mjs +138 -0
  288. package/tools/verify-owned-runtime-release.mjs +114 -0
  289. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """Paired real-PTY glyph probe. Only known host chrome is normalized/asserted."""
3
+ import argparse
4
+ import fcntl
5
+ import hashlib
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import pty
10
+ import re
11
+ import select
12
+ import signal
13
+ import struct
14
+ import subprocess
15
+ import tempfile
16
+ import termios
17
+ import time
18
+ import importlib.util
19
+ import sys
20
+
21
+ _helper_path = Path(__file__).with_name('native-glyph-helpers.py')
22
+ _helper_spec = importlib.util.spec_from_file_location('native_glyph_helpers', _helper_path)
23
+ if _helper_spec is None or _helper_spec.loader is None:
24
+ raise ImportError(f'Could not load native glyph helper module at {_helper_path}')
25
+ _helpers = importlib.util.module_from_spec(_helper_spec)
26
+ sys.modules[_helper_spec.name] = _helpers
27
+ _helper_spec.loader.exec_module(_helpers)
28
+ CHROME_PAIRS = _helpers.CHROME_PAIRS
29
+ USER_TEXT = _helpers.USER_TEXT
30
+ GlyphScreen = _helpers.GlyphScreen
31
+ assert_composer_payload = _helpers.assert_composer_payload
32
+ child_environment = _helpers.child_environment
33
+
34
+
35
+ def run_mode(binary, mode, timeout):
36
+ with tempfile.TemporaryDirectory(prefix='ctg-glyph-pty-') as temp:
37
+ root = Path(temp)
38
+ project = root / 'project'
39
+ config = root / 'config' / 'opencode'
40
+ for directory in (project, config, root / 'home', root / 'data', root / 'state', root / 'cache'):
41
+ directory.mkdir(parents=True, exist_ok=True)
42
+ (config / 'opencode.json').write_text(json.dumps({'update': 'disable', 'model': 'ctg-glyph/fixture',
43
+ 'providers': {'ctg-glyph': {'name': 'Local glyph probe',
44
+ 'package': '@opencode/ai/providers/openai-compatible',
45
+ 'settings': {'baseURL': 'http://127.0.0.1:1/v1', 'apiKey': 'offline-fixture'},
46
+ 'models': {'fixture': {'name': 'Glyph fixture', 'limit': {'context': 32000, 'output': 1024}}}}}}), encoding='utf8')
47
+ env = child_environment(root, config, mode)
48
+ master, slave = pty.openpty()
49
+ fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 32, 100, 0, 0))
50
+ child = subprocess.Popen([binary, '--standalone'], cwd=project, env=env,
51
+ stdin=slave, stdout=slave, stderr=slave, start_new_session=True)
52
+ os.close(slave)
53
+ screen = GlyphScreen()
54
+ raw = bytearray()
55
+ started = time.monotonic()
56
+ pasted = False
57
+ home_seen = False
58
+ try:
59
+ while time.monotonic() - started < timeout:
60
+ if child.poll() is not None:
61
+ raise RuntimeError(f'{mode}: runtime exited {child.returncode}: {screen.visible()[-1200:]}')
62
+ if select.select([master], [], [], .05)[0]:
63
+ try:
64
+ chunk = os.read(master, 65536)
65
+ except OSError:
66
+ break
67
+ raw.extend(chunk)
68
+ screen.feed(chunk)
69
+ if b'\x1b[6n' in chunk:
70
+ os.write(master, b'\x1b[1;1R')
71
+ if b'\x1b[c' in chunk:
72
+ os.write(master, b'\x1b[?1;2c')
73
+ visible = screen.visible()
74
+ lower = visible.lower()
75
+ if not pasted and ('shift+tab agents' in lower and 'ctrl+p commands' in lower):
76
+ home_seen = True
77
+ # Insert but do not submit: this makes arbitrary user text
78
+ # observable in the genuine composer without contacting a provider.
79
+ os.write(master, b'\x1b[200~' + USER_TEXT.encode() + b'\x1b[201~')
80
+ pasted = True
81
+ if pasted and any(re.fullmatch(r'\s*[┃|]\s*' + re.escape(USER_TEXT) + r'\s*', row)
82
+ for row in screen.visible().splitlines()):
83
+ break
84
+ visible = screen.visible()
85
+ if not home_seen or not pasted:
86
+ raise AssertionError(f'{mode}: home/composer was not reached; screen={visible[-1600:]!r}')
87
+ composer_row = assert_composer_payload(visible)
88
+ # Exclude the rendered composer row from static chrome checks.
89
+ lines = visible.splitlines()
90
+ marker_rows = [index for index, line in enumerate(lines) if USER_TEXT in line]
91
+ chrome_region = '\n'.join(line for index, line in enumerate(lines) if index not in marker_rows)
92
+ chrome = {}
93
+ for unicode_token, ascii_token in CHROME_PAIRS:
94
+ chrome[unicode_token] = {'asciiAlternative': ascii_token,
95
+ 'unicodePresent': unicode_token in chrome_region,
96
+ 'asciiPresent': ascii_token in chrome_region}
97
+ if mode == 'ascii':
98
+ if CHROME_PAIRS[0][0] in chrome_region or '▀▀▀' in chrome_region:
99
+ raise AssertionError(f'ASCII startup logo still contains Unicode block-art chrome; screen={visible[-1200:]!r}')
100
+ if 'CARTHAGENT' not in chrome_region.replace(' ', ''):
101
+ raise AssertionError(f'ASCII startup logo alternative not observed; screen={visible[-1200:]!r}')
102
+ else:
103
+ if CHROME_PAIRS[0][0] not in chrome_region:
104
+ raise AssertionError(f'Unicode startup logo not observed; screen={visible[-1200:]!r}')
105
+ if 'Carthagent' not in screen.title_updates:
106
+ raise AssertionError(f'{mode}: Carthagent terminal title not observed: {screen.title_updates!r}')
107
+ if any('opencode' in title.lower() for title in screen.title_updates):
108
+ raise AssertionError(f'{mode}: upstream terminal title remains: {screen.title_updates!r}')
109
+ if 'opencode' in chrome_region.lower():
110
+ raise AssertionError(f'{mode}: upstream branding remains on the home screen')
111
+ return {'mode': mode, 'homeObserved': home_seen, 'terminalTitles': screen.title_updates, 'composerPayloadExact': True,
112
+ 'composerRow': composer_row,
113
+ 'userText': USER_TEXT, 'hostChrome': chrome, 'screen': visible,
114
+ 'ptyOutputSha256': hashlib.sha256(raw).hexdigest(), 'ptyOutputBytes': len(raw)}
115
+ finally:
116
+ if child.poll() is None:
117
+ os.write(master, b'\x1b')
118
+ os.killpg(child.pid, signal.SIGTERM)
119
+ try:
120
+ child.wait(timeout=3)
121
+ except subprocess.TimeoutExpired:
122
+ os.killpg(child.pid, signal.SIGKILL)
123
+ child.wait()
124
+ os.close(master)
125
+
126
+
127
+ def main():
128
+ parser = argparse.ArgumentParser()
129
+ parser.add_argument('--binary', required=True)
130
+ parser.add_argument('--timeout', type=int, default=45)
131
+ parser.add_argument('--artifact')
132
+ args = parser.parse_args()
133
+ modes = [run_mode(args.binary, mode, args.timeout) for mode in ('ascii', 'unicode')]
134
+ result = {'status': 'PASS', 'claim': 'paired actual-TUI PTY rendering; only known static chrome is asserted',
135
+ 'provider': 'none; prompt is staged but not submitted', 'physicalClipboardVerified': False,
136
+ 'modes': modes}
137
+ text = json.dumps(result, ensure_ascii=False, indent=2) + '\n'
138
+ print(text, end='')
139
+ if args.artifact:
140
+ Path(args.artifact).resolve().write_text(text, encoding='utf8')
141
+
142
+
143
+ if __name__ == '__main__':
144
+ main()
@@ -0,0 +1,259 @@
1
+ import { mkdir, writeFile, readdir, readFile, rm } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ export async function createManualProjects(
8
+ destination,
9
+ { packages = 128 } = {},
10
+ ) {
11
+ if (!Number.isInteger(packages) || packages < 2 || packages > 1024)
12
+ throw Error('packages must be an integer from 2 to 1024');
13
+ const root = resolve(destination);
14
+ await mkdir(dirname(root), { recursive: true });
15
+ await mkdir(root);
16
+ const write = async (project, file, contents) => {
17
+ const path = join(root, project, file);
18
+ await mkdir(dirname(path), { recursive: true });
19
+ await writeFile(path, contents);
20
+ };
21
+ const packageFile = (name, extra = {}) =>
22
+ JSON.stringify(
23
+ {
24
+ name,
25
+ version: '0.0.0',
26
+ private: true,
27
+ type: 'module',
28
+ scripts: {
29
+ test: 'node --test test/*.test.mjs',
30
+ 'test:acceptance': 'node --test acceptance/*.test.mjs',
31
+ ...extra,
32
+ },
33
+ },
34
+ null,
35
+ 2,
36
+ ) + '\n';
37
+ const imports =
38
+ "import {test} from 'node:test';\nimport assert from 'node:assert/strict';\n";
39
+ try {
40
+ await write('tiny', 'package.json', packageFile('ctg-invoice-fixture'));
41
+ await write(
42
+ 'tiny',
43
+ 'src/invoice.mjs',
44
+ 'export function invoiceTotal(lines) { return lines.reduce((total, line) => total + line.quantity * line.unitPrice, 0); }\n',
45
+ );
46
+ await write(
47
+ 'tiny',
48
+ 'test/invoice.test.mjs',
49
+ imports +
50
+ "import {invoiceTotal} from '../src/invoice.mjs';\ntest('empty invoices',()=>assert.equal(invoiceTotal([]),0));\ntest('integer totals',()=>assert.equal(invoiceTotal([{quantity:2,unitPrice:500}]),1000));\n",
51
+ );
52
+ await write(
53
+ 'tiny',
54
+ 'acceptance/discount.test.mjs',
55
+ imports +
56
+ "import {invoiceTotal} from '../src/invoice.mjs';\ntest('discount in basis points, rounded once to cents',()=>assert.equal(invoiceTotal([{quantity:3,unitPrice:199}],{discountBps:1500}),507));\ntest('reject invalid discounts',()=>assert.throws(()=>invoiceTotal([],{discountBps:10001}),RangeError));\ntest('immutable inputs',()=>assert.equal(invoiceTotal(Object.freeze([Object.freeze({quantity:1,unitPrice:999})]),{discountBps:1000}),899));\n",
57
+ );
58
+ await write(
59
+ 'tiny',
60
+ 'TASK.md',
61
+ '# Small task\nAdd optional {discountBps} to invoiceTotal. Prices/totals are integer cents. Accept integer discounts 0..10000; reject others with RangeError. Round once after discount. Preserve old calls and never mutate inputs. Run npm test and npm run test:acceptance. Baseline is green; new-feature acceptance is intentionally red.\n',
62
+ );
63
+
64
+ await write(
65
+ 'product',
66
+ 'package.json',
67
+ packageFile('ctg-forgeboard-fixture', { start: 'node server.mjs' }),
68
+ );
69
+ await write(
70
+ 'product',
71
+ 'src/issues.mjs',
72
+ "export const initialIssues=()=>[{id:1,title:'Design onboarding',status:'todo',revision:1},{id:2,title:'Fix keyboard navigation',status:'doing',revision:1}];\nexport const selectIssues=(issues,status)=>issues.filter(issue=>!status||issue.status===status);\n",
73
+ );
74
+ await write(
75
+ 'product',
76
+ 'server.mjs',
77
+ `import {createServer} from 'node:http';
78
+ import {readFile} from 'node:fs/promises';
79
+ import {pathToFileURL} from 'node:url';
80
+ import {resolve} from 'node:path';
81
+ import {initialIssues,selectIssues} from './src/issues.mjs';
82
+ export function createApp(){
83
+ const issues=initialIssues();
84
+ return createServer(async(request,response)=>{
85
+ const url=new URL(request.url,'http://localhost');
86
+ if(request.method==='GET'&&url.pathname==='/api/issues'){response.writeHead(200,{'content-type':'application/json'});response.end(JSON.stringify(selectIssues(issues,url.searchParams.get('status'))));return;}
87
+ if(request.method==='GET'&&url.pathname==='/'){response.writeHead(200,{'content-type':'text/html; charset=utf-8'});response.end(await readFile(new URL('./public/index.html',import.meta.url)));return;}
88
+ response.writeHead(404,{'content-type':'application/json'});response.end(JSON.stringify({error:'not found'}));
89
+ });
90
+ }
91
+ if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href){const server=createApp();server.listen(Number(process.env.PORT??3017),'127.0.0.1',()=>console.log('Forgeboard: http://127.0.0.1:'+server.address().port));}
92
+ `,
93
+ );
94
+ await write(
95
+ 'product',
96
+ 'public/index.html',
97
+ '<!doctype html><html lang="en"><meta charset="utf-8"><title>Forgeboard</title><style>body{font:18px system-ui;background:#f4f1e9;color:#202923;max-width:900px;margin:64px auto;padding:24px}header{border-bottom:2px solid #202923}li{background:white;padding:20px;margin:12px 0;list-style:none}select{font:inherit;padding:8px}</style><header><small>CARTHAGENT TEST PROJECT / 02</small><h1>Forgeboard</h1><p>A disposable issue board. No external services.</p></header><p><label>Status <select id="status"><option value="">All</option><option>todo</option><option>doing</option><option>done</option></select></label></p><ul id="issues"></ul><script type="module">async function load(){const issues=await(await fetch("/api/issues?status="+encodeURIComponent(document.querySelector("select").value))).json();document.querySelector("ul").replaceChildren(...issues.map(issue=>{const li=document.createElement("li");li.textContent=issue.title+" · "+issue.status+" · revision "+issue.revision;return li}));}document.querySelector("select").onchange=load;load();</script></html>\n',
98
+ );
99
+ const apiTest =
100
+ "import {createApp} from '../server.mjs';\nasync function appFor(t){const app=createApp();await new Promise(r=>app.listen(0,'127.0.0.1',r));t.after(()=>new Promise(r=>app.close(r)));return 'http://127.0.0.1:'+app.address().port;}\n";
101
+ await write(
102
+ 'product',
103
+ 'test/api.test.mjs',
104
+ imports +
105
+ apiTest +
106
+ "test('real HTTP list, filtering, UI and 404',async t=>{const base=await appFor(t);assert.equal((await(await fetch(base+'/api/issues')).json()).length,2);assert.equal((await(await fetch(base+'/api/issues?status=doing')).json())[0].id,2);assert.match(await(await fetch(base)).text(),/Forgeboard/);assert.equal((await fetch(base+'/missing')).status,404);});\n",
107
+ );
108
+ await write(
109
+ 'product',
110
+ 'acceptance/update.test.mjs',
111
+ imports +
112
+ apiTest +
113
+ "test('optimistic concurrency protects competing status updates',async t=>{const base=await appFor(t);const patch=(revision,status)=>fetch(base+'/api/issues/1',{method:'PATCH',headers:{'content-type':'application/json'},body:JSON.stringify({revision,status})});const first=await patch(1,'done');assert.equal(first.status,200);assert.equal((await first.json()).revision,2);assert.equal((await patch(1,'doing')).status,409);assert.equal((await patch(2,'invalid')).status,400);const list=await(await fetch(base+'/api/issues?status=done')).json();assert.equal(list[0].id,1);assert.equal(list[0].status,'done');});\n",
114
+ );
115
+ await write(
116
+ 'product',
117
+ 'TASK.md',
118
+ '# Product task\nImplement PATCH /api/issues/:id accepting JSON {status,revision}. Allow todo/doing/done. Stale revisions ->409; invalid input/JSON ->400; unknown ids ->404; oversized bodies ->413. Increment revision exactly once on success. Add accessible status controls and error feedback to the board. Keep in-memory storage; no external services/dependencies. Preserve safe text rendering. Add malformed/oversized/unknown-id tests beyond supplied acceptance. Run npm test and npm run test:acceptance. Start npm start and inspect http://127.0.0.1:3017 manually.\n',
119
+ );
120
+
121
+ await write(
122
+ 'scale',
123
+ 'package.json',
124
+ packageFile('ctg-scale-monorepo', {
125
+ 'test:stress': 'node --test --test-concurrency=2 test/*.test.mjs',
126
+ }),
127
+ );
128
+ await write(
129
+ 'scale',
130
+ 'shared/money.mjs',
131
+ 'export function roundMoney(value){return Math.round(value*100)/100;}\n',
132
+ );
133
+ await write(
134
+ 'scale',
135
+ 'test/packages.test.mjs',
136
+ imports +
137
+ Array.from(
138
+ { length: packages },
139
+ (_, i) =>
140
+ `test('domain-${i} integration',async()=>{const {total}=await import('../packages/domain-${i}/src/index.mjs');assert.equal(total([1,2,3]),6);assert.equal(total([]),0);});`,
141
+ ).join('\n') +
142
+ '\n',
143
+ );
144
+ for (let i = 0; i < packages; i++) {
145
+ await write(
146
+ 'scale',
147
+ `packages/domain-${i}/package.json`,
148
+ JSON.stringify({
149
+ name: `@fixture/domain-${i}`,
150
+ private: true,
151
+ type: 'module',
152
+ }) + '\n',
153
+ );
154
+ await write(
155
+ 'scale',
156
+ `packages/domain-${i}/src/index.mjs`,
157
+ "import {roundMoney} from '../../../shared/money.mjs';\nexport const total=values=>roundMoney(values.reduce((sum,value)=>sum+value,0));\n",
158
+ );
159
+ for (let j = 0; j < 8; j++)
160
+ await write(
161
+ 'scale',
162
+ `packages/domain-${i}/src/metric-${j}.mjs`,
163
+ `import {total} from './index.mjs';\nexport const metric${j}=values=>({domain:${i},metric:${j},total:total(values)});\n`,
164
+ );
165
+ await write(
166
+ 'scale',
167
+ `docs/domain-${i}.md`,
168
+ `# Domain ${i}\nUses shared/money.mjs through its public index. Preserve domain identity.\n`,
169
+ );
170
+ }
171
+ await write(
172
+ 'scale',
173
+ 'acceptance/rounding.test.mjs',
174
+ imports +
175
+ "import {roundMoney} from '../shared/money.mjs';\nimport {total} from '../packages/domain-0/src/index.mjs';\ntest('decimal halves away from zero across shared consumers',()=>{assert.equal(roundMoney(1.005),1.01);assert.equal(roundMoney(-1.005),-1.01);assert.equal(total([1.005]),1.01);});\ntest('reject nonfinite amounts',()=>{for(const v of [NaN,Infinity,-Infinity])assert.throws(()=>roundMoney(v),RangeError);});\n",
176
+ );
177
+ await write(
178
+ 'scale',
179
+ 'TASK.md',
180
+ `# Large monorepo task\n${packages} packages share money rounding; do not rewrite generated packages. Fix representable decimal halves (1.005 ->1.01, -1.005 ->-1.01, ties away from zero); reject nonfinite numbers with RangeError. Preserve existing behavior, avoid dependencies, and add boundary regressions. Run npm test and npm run test:acceptance. Use code navigation to find affected consumers and verify at least three distinct domains.\n`,
181
+ );
182
+ const inventory = {};
183
+ for (const name of ['tiny', 'product', 'scale']) {
184
+ await write(name, '.gitignore', 'node_modules/\n.harness/\n.env\n');
185
+ const files = [];
186
+ const visit = async (directory) => {
187
+ for (const entry of (
188
+ await readdir(join(root, name, directory), { withFileTypes: true })
189
+ ).sort((a, b) => a.name.localeCompare(b.name))) {
190
+ const file = directory ? `${directory}/${entry.name}` : entry.name;
191
+ if (entry.isDirectory()) await visit(file);
192
+ else
193
+ files.push({
194
+ path: file,
195
+ sha256: createHash('sha256')
196
+ .update(await readFile(join(root, name, file)))
197
+ .digest('hex'),
198
+ });
199
+ }
200
+ };
201
+ await visit('');
202
+ inventory[name] = { files: files.length, source: files };
203
+ const git = (...args) =>
204
+ execFileSync('git', ['-c', 'core.hooksPath=disabled-hooks', ...args], {
205
+ cwd: join(root, name),
206
+ stdio: 'pipe',
207
+ });
208
+ git('init', '-b', 'main');
209
+ git('add', '.');
210
+ git(
211
+ '-c',
212
+ 'user.name=CTG manual fixture',
213
+ '-c',
214
+ 'user.email=fixture@localhost',
215
+ 'commit',
216
+ '-m',
217
+ 'Initialize disposable manual-test project\n\nCo-authored-by: openhands <openhands@all-hands.dev>',
218
+ );
219
+ }
220
+ await writeFile(
221
+ join(root, 'inventory.json'),
222
+ JSON.stringify({ version: 1, packages, projects: inventory }, null, 2) +
223
+ '\n',
224
+ );
225
+ return {
226
+ root,
227
+ packages,
228
+ projects: Object.fromEntries(
229
+ Object.entries(inventory).map(([name, value]) => [
230
+ name,
231
+ { files: value.files },
232
+ ]),
233
+ ),
234
+ };
235
+ } catch (error) {
236
+ await rm(root, { recursive: true, force: true });
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ if (
242
+ process.argv[1] &&
243
+ import.meta.url === pathToFileURL(resolve(process.argv[1])).href
244
+ ) {
245
+ const [destination, packages] = process.argv.slice(2);
246
+ if (!destination)
247
+ throw Error(
248
+ 'Usage: node tools/native-manual-projects.mjs <new-directory> [packages=128]',
249
+ );
250
+ console.log(
251
+ JSON.stringify(
252
+ await createManualProjects(destination, {
253
+ packages: Number(packages ?? 128),
254
+ }),
255
+ null,
256
+ 2,
257
+ ),
258
+ );
259
+ }