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.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/launch-readiness-live-results.md +65 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +175 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-beta.1.md +60 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +22 -5
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/tui/framing.mjs +1 -80
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +59 -31
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -0
- package/plugins/carthagent/src/ui/client.ts +127 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +64 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- package/tests/delivery.test.mjs +22 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +55 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +525 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-guidance.test.mjs +16 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +71 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +597 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +231 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +43 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +876 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +87 -63
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- 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 '<script>alert("x")</script> & "quoted"' 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
|
|
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, '../
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
24
|
-
const
|
|
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
|
|
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
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
113
|
-
|
|
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
|
-
&&
|
|
117
|
-
&& /(
|
|
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:
|
|
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) {
|