CRW-152 · Go port wave 2 통합: store + bridge + doctor/status - #172
Merged
Merged
Conversation
Inventory of 117 non-test Python files mapped to CRW-150..161 owners with the process-spawn graph; scripts/port/check_inventory.py reconciles it with the tree.
CRW-140: 19 decisions, each with an Evidence line citing file:line or a URL.
CRW-140: 119 test files / 6,041 tests classified A/B/C with property, fixtures and Go destination; scripts/port/check_test_map.py reconciles counts with the tree.
CRW-150: go.mod (go 1.27, toolchain go1.27.1), staticcheck pinned at honnef.co/go/tools v0.7.0 via tools.go, Makefile build/test/contract/lint/dist with CGO_ENABLED=0 guard and empty-module skips.
…cts as data CRW-140: contract/schema/** generated by scripts/port/dump_contracts.py from the running Python (argparse tree, Store DDL and PRAGMAs, live MCP tools/list, App Server methods and bounds, hook and record shapes). --check regenerates and fails with a DIFF on any drift; the CLI count reconciles 144 top-level / 152 nodes / 140 call sites.
…ontract CRW-140: docs/port/cutover.md (ownership record, lock order, steps 0-7 with failure branches, same-DB rollback, rollback_allowed commit point, takeover inbox, hook budget, retention and its scan surface) and docs/port/control-group.md (8 CRW-116 + 19 CRW-124 scenarios transcribed verbatim from Linear, each with a Pass when line). scripts/port/check_cutover_doc.py enforces the sections, the scenario numbering and that no TTL or heartbeat authorises takeover.
CRW-150: cmd/crw dispatches on argv[0] (codex-session-relay, codex-thread-bridge, crw-completion-hook) then subcommand; internal/contract Emit reproduces Python json.dumps byte for byte (ordered maps, Python float repr) against Python-produced goldens; exit codes and 114 refusal reasons generated from contract/schema/relay-exit-codes.json; command registry with ContinueOnError flags (exit 4, nothing on stdout); scripts/port/check_json_emit.sh keeps JSON emission inside internal/contract. Makefile builds ./cmd/crw; staticcheck pinned to v0.8.1 for go1.27.
CRW-150: internal/bridge/appserver/fakehost speaks the App Server envelope over a unix-socket websocket (handshake as rpc.py: initialize then initialized; refused approval methods; 16 MiB frame limit, close 1009; frames with a jsonrpc member recorded as malformed), ported from the Python FakeServer in packages/codex-thread-bridge/tests/conftest.py. internal/testsupport carries the shared helpers. Adds github.com/coder/websocket v1.8.15.
CRW-150: .goreleaser.yaml (v2) builds ./cmd/crw with CGO_ENABLED=0 for linux/amd64, linux/arm64, darwin/arm64 into archives carrying crw, its three compatibility symlinks and both LICENSE files, with SHA256SUMS. release.yml gains release-go after the existing verification, gated like the other publish steps, and a snapshot build on the dry-run path. docs/releases.md labels darwin/arm64 built but unvalidated.
This reverts commit 9f518ae.
…thon build CRW-140 todo 6. contract/fixtures holds 350 scenarios run by contract/runner (hook, CLI, MCP, git, App Server, release, file observations) against the real Python programs, collected by both packages' test_contract_corpus.py through the repo-root conftest.py. Class-A tests that converted are thin runners over their fixture; each of the 555 class-A functions is recorded in contract/notes as converted, kept (live interleave, or mutation proofs moving to Go tests in todo 9) or blocked with the missing runner kind, and kept or blocked cases are Go test obligations of the owning todo named in docs/port/test-map.md. scripts/port/check_corpus_count.py enforces that coverage (criterion amended by Jun: behaviour coverage, not a fixture count). Relay CLI runs set HOME, XDG_* and CODEX_HOME to the scenario temp dir. Also registers the scripts/port check scripts added by todos 2, 3, 5 and 6 in docs/port/inventory.md, which todo 1's check had been missing.
CRW-150 todo 9. internal/contracttest finds the checkout through runtime.Caller(0), loads contract/fixtures/<domain>/*.json in place (no copies, no embedding) and runs registered kinds against the built crw (CLI argv and stdin; seeded SQL and row observations through modernc.org/sqlite). Domains and kinds without a Go implementation are explicit skips, counted and printed as 'skipped: <n> domains not ported'; CRW_CONTRACT_STRICT=1 turns any skip into a failure naming the domains. No domain is registered as ported yet.
CRW-151 todo 13. internal/bridge/appserver dials the App Server over a unix-socket websocket (compression off), performs the initialize/initialized handshake, correlates responses by id, routes notifications, leaves approval-class server requests unanswered for the thread's approver (recorded as left) and answers every other server request with -32601 (recorded as refused), each in its own 64-entry ring, as rpc.py:318-346 does. Phase bounds establish/transmit/ack, a 16 MiB frame limit reported as ResponseTooLarge without naming the request, a peer 1009 kept distinct from the local limit, reconnect only for new requests, and a 2s close. The appserver corpus domain runs through internal/contracttest.
CRW-152 todo 17. internal/relay/store opens the relay database with the frozen contract/schema/relay-sqlite.sql embedded byte-for-byte (a test compares it with the contract file), the guard indexes derived from it, foreign_keys/WAL/synchronous=FULL/busy_timeout on every pooled connection, insert-or-ignore identity rows, and a refusal to open the live host state directory (symlinks resolved). State-directory selection (--state, CODEX_SESSION_RELAY_STATE, XDG_STATE_HOME, socket hash, legacy sibling adoption) and Locate follow the Python resolve_state_dir, discover_state_dir and locate, checked against the real Python, including '..' spellings, symlinks and an unknown ~user (an error, as pathlib raises). The sqlite-ddl corpus domain now checks the DDL, a committed Python-created database (scripts/port/make_sqlite_fixture.py), the pooled PRAGMAs and lock behaviour in both directions. test_store.py properties move to todo 18 in docs/port/test-map.md.
…ath job Todo 11 (997cae8) added two class-A test_release.py functions and three other CI test functions and grew scripts/ci/scope.py by a line without updating the port maps, so check_test_map, check_corpus_count and check_inventory failed on this branch. The two release-workflow tests are kept in Python with todo 47 as their Go owner; the counts and the scope.py line are updated. All three checks exit 0.
# Conflicts: # docs/port/inventory.md
CRW-151 todo 14. internal/bridge/ledger keeps the operation ledger byte-compatible with the Python one: the same canonical JSON fingerprint (sorted keys, ensure_ascii escaping including lone surrogates, Python float rendering, -0 and overflow), 39 Python-generated goldens, a Python-written ledger database replayed to the identical receipt, request ids checked in characters on Begin and Lookup, only not_attempted receipts re-armed, and the socket alias resolved as Path.resolve does. internal/bridge/settings and internal/bridge/execution port the settings contract and execution policy; 59 of the 103 test_settings.py and test_execution.py tests are Go subtests named after them, and the 44 that observe what reaches the host through create/resume/worktree are verified in todo 15 (docs/port/test-map.md). scripts/port/make_ledger_fixture.py regenerates the ledger-fingerprint corpus fixtures from the real Python ledger.
# Conflicts: # docs/port/inventory.md
CRW-152 todo 18. internal/relay/store gains typed record functions for relationships, generations, events, deliveries, attempts, acks, verdicts, journal and receipts; the full Python receipt intake (refusal reasons, turn admission, contradiction checks, staging, daemon observations, recorded refusals); artifact reads that refuse symlinks, read twice and detect relocation; the registry's set-status, supersede, open-generation and bind-anchor with a relationship record byte-identical to Python's; and store identity (locate, probe, nonce lookup, challenge) in Python's check order. The store keeps one SQLite connection (docs/port/decisions.md section 4). Transaction carries the open transaction in the context: a nested Transaction is refused at once, Compose joins, and every read goes through the transaction's connection so it sees uncommitted writes, as Python's single connection does. Behaviour parity: 172 of the 178 owned Python tests are equivalent Go tests; 6 depend on the delivery service and are deferred to todo 21 with their store half tested here. Persisted records are validated with the repository's Python jsonschema, so the go-product CI job now syncs the uv workspace before running the Go tests.
CRW-151 todo 15. internal/bridge ports the Python bridge core (create, read, observe, steer, pause, retry, cancel, relabel, capabilities, sandbox policy) and internal/bridge/worktrees the owned-worktree lifecycle, including the fake-host no-dispatch properties of test_settings.py and test_execution.py deferred from todo 14. 145 of the 146 mapped Python tests are equivalent Go tests; one is deferred to todo 16 (docs/port/test-map.md). internal/contracttest gains the git-backed Domain steps (30/30, no skips). Caller-visible strings copied from Python keep their text under //lint:ignore ST1005 naming the Python line, which also clears the staticcheck failures on the todo-13/14 tree. Verified in a clean copy of HEAD plus this diff: uv sync --locked, make lint, make test and make contract each exit 0.
Plan item 15a, found by CI on PR #170 (run 36194600153). worktrees.git called the AfterGit seam only when args[0] named the command, but Checkout prepends "-c filter.<name>.*" pairs whenever a git filter is configured. GitHub runners configure filter.lfs globally, so the seam never fired for checkout-index and the cancellation test timed out with "Git stage not reached". The seam now skips leading -c pairs; the git commands and their arguments are unchanged. Test_checkout_index_cancellation_with_global_lfs_filter runs the checkout-index cancellation path with HOME holding a filter.lfs gitconfig: it fails without the fix and passes with it. Clean copy: uv sync --locked, make lint, make test, make contract and the bridge suite under the lfs HOME each exit 0.
… without pytest Plan item 15b, found by CI on PR #170 and failing identically on #168 and #169 while dev was green: - validate: wave 0 edited packages/*/tests without re-deriving scripts/crw_runtime/components.json, so verify-definition reported both subdirectoryTree values stale; they are re-derived with git rev-parse HEAD:packages/<name> as docs/runtime-install.md describes. The install KeyErrors in scripts/ci/tests followed from the same stale definition. - tests: contract/runner imported pytest at load time and loaded stopadapter through the relay package; the offline scripts/ci/tests job has neither. pytest.fail becomes AssertionError and stopadapter.py is loaded from its file, as the pre-wave-0 test did. The CI job itself is unchanged (POLICY.md: standard-library checks). - dev-gate: contract/, docs/port/ and conftest.py had no verification mapping; they are registered for full coverage because tests and scripts/port checkers read them. Verified in a clean copy under a Python 3.10 venv without pytest: scripts/ci/validate.py, contracts.py, plugin.py and python3 -m unittest discover -s scripts/ci/tests each exit 0.
# Conflicts: # docs/CI.md # scripts/ci/scope.py
# Conflicts: # contract/runner/core.py
CRW-152 todo 19. internal/relay/store gains typed queries for the 59 tables the core records left out (linkage, merge-turn, capacity, edit regions, managed start, supervisor channel, sync, product routing and the fault ledger): 209 methods, each naming the Python store/domain line it reproduces, keeping its columns, ORDER BY, conflict clause and literal defaults. Every write and read goes through the transaction-aware querier. TestEverySchemaTable_has_a_go_query_referencing_it parses the embedded schema (84 tables; the plan's 90 counted CREATE TABLE inside comments, checked against sqlite_master) and fails for any table without a Go query. Ten parity tests read stores written by the real Python relay, one per domain group, and compare every column. Guard indexes: under racing threads Python's writers refuse, queue or replay before any of the six partial-unique indexes is reached; only a raw INSERT reaches one, and Python then raises sqlite3.IntegrityError (2067, "UNIQUE constraint failed: ..."). Go returns the same driver error, and each TestGuard_<index>_fails_like_python drives Python into that index first. Independent checker confirmed round 2 (3/3 mutations caught after 11/13 in round 1). Clean copy: uv sync --locked, make lint, make test, make contract and check_test_map each exit 0.
# Conflicts: # docs/port/inventory.md # internal/contracttest/registry.go
CRW-151 todo 16. internal/bridge/mcp serves the 12 bridge tools over stdio with go-sdk v1.8.0 typed handlers, and `crw bridge [--socket] [--state]` (also reached as codex-thread-bridge) runs it; stdout carries only MCP frames and every log line goes to stderr. A pythonWire middleware keeps the wire equal to the Python FastMCP server: tools/list and the empty prompt/resource lists carry no SDK cache fields, initialize advertises Python's capabilities, unknown methods and the prompt/resource/logging/completion errors answer as mcp 1.30.0 does, and argument validation reports every failing field in signature order behind Python's "Error executing tool <t>: N validation error(s) for <t>Arguments" prefix. internal/bridge/pyerr renders OS errors as CPython does (PEP 3151 classes, strerror, filename suffix). internal/contracttest runs the 11 mcp-tools fixtures against the built binary (plain and strict); the live tools/list equals contract/schema/bridge-mcp-tools.json after mcpnorm, which compares the whole result and forgives only $schema, key order and additionalProperties:true; 30 replies equal recorded live Python output as whole JSON. Known differences pending a decision: serverInfo.version (0.1.0, Python sends the mcp library version), the per-field validation body text (pydantic's own wording), and key order inside the reply text. Independent checker confirmed round 2 (10/10 mutations). Clean copy: uv sync --locked, make lint, make test, make contract, strict Domain/mcp-tools and check_test_map each exit 0.
# Conflicts: # internal/contracttest/registry.go
…not reproduce Decision 20: pydantic validation body, serverInfo.version, reply-text key order and the CPython JSON decoder message stay Go-written while prefixes, counts, field order, codes and exits match Python. Taken as the recommended option after the question to Jun went unanswered; reversible.
Plan item 16a, from the todo-16 checker. pythonConnection.Read now matches mcp 1.30.0 for frames a conforming client never sends: a message whose params are present and neither null nor an object gets only the notifications/message "Internal Server Error" log frame and no reply (unknown and known notifications, known requests such as ping [1]); a tools/call whose name is not a string or whose arguments are not an object gets -32602 "Invalid request parameters" with data "". tools/list is written without HTML escaping, so '<' and '>' match Python's bytes. Python's frames are recorded raw by testdata/gen_wire_python.py. Five new wire tests fail with the previous middleware and pass with this one. Clean copy: uv sync --locked, make lint, make test, make contract and strict Domain/mcp-tools each exit 0.
CRW-152 todo 20. `crw relay doctor [--require-worker-policy --expect-store --expect-inode --expect-log --expect-nonce --issue]`, `store-identity`, `store-challenge --write/--read/--actor`, `show --event|--message` and `status --relationship` answer with Python's JSON byte for byte; `crw` gains argparse-shaped help/version/usage exits alongside the relay and bridge modes. The 32 test_diagnostics.py tests become 23 properties: each Go subtest runs the real Python test first and requires Go's whole `status` JSON to equal Python's on that store; `show` runs, including `--message` previews of unattempted deliveries (internal/relay/delivery/preview.go, reusable by todo 21), are compared the same way. The recovery-line program is asserted independently of the code under test. Doctor reads execution policy through internal/bridge/execution and matches Python with the policy in the environment and declared in launch-policy.json; the side-by-side diff with codex-session-relay doctor is empty except a new last top-level `runtime` key, which Python's report has no slot for. `--expect-inode` mismatch exits 2 with Python's detail; an unknown ~user in --state exits 3 with Python's host error; `--kind-module` exits 4/3 as Python does until todo 22's static registry. Fault attention reads inside a store transaction. Deferred by the plan: workerPolicy for a receipt matching this installation (todo 29 daemon), composed work-report and merge-grant previews (todo 21 renderers). Built on the integration base (store + todo 19 + bridge). Independent checker confirmed round 2. Clean copy: uv sync --locked, make lint, make test, make contract, strict Domain/mcp-tools and the doctor cli-shape fixtures, check_inventory, check_test_map and check_corpus_count each exit 0.
…ng requests Plan item 15c, found by CI on PR #172. appserver.Dial returns on the initialize response, while the fake host records the client's `initialized` notification asynchronously; the restart-replay test counted requests in between, so its exact no-resend count raced (402 failures in 300 runs at -cpu 1,2,4 with the old test, 1 in 30 at the default). fakehost.WaitCount blocks, signalled by the host's own recording path and bounded by a context, until a method has been recorded N times; the replay test and the two sibling handshake-order assertions wait for it. The no-resend assertions are unchanged, and the seven other request-count sites were checked and read counts only after completed calls. After the change: 300 runs at -cpu 1,2,4 with no failure; clean copy uv sync --locked, make lint, make test and make contract exit 0.
Plan item 15d, found by CI on PR #170 (Test_test_cancellation_keeps_unknown_receipt_and_prevents_retry: "cancellation not propagated: <nil>"). appserver's request selected between the reader's outcome and ack.Done; when cancellation closed the socket during the write, or a response or RPC error arrived at the same moment, the reader branch could win, and the bridge recorded that ordinary failure as a receipt and returned nil. Python raises CancelledError from the pending call, keeps an outcome_unknown receipt and replays it without a second dispatch. The request now checks its context after either select branch and before interpreting a reader outcome, so cancellation wins over a response, an RPC error or a disconnect; a deadline still reports the ack PhaseTimeout. WithOutcomeHook is a context-scoped test seam that cancels after the reader branch is chosen; the new disconnect/response/rpc_error cases fail with the previous client and pass with this one. Verified: the new cases fail 3/3 with the old client.go; 500 runs at -cpu 1,2,4 without a failure; clean copy uv sync --locked, make lint, make test, make contract and strict appserver/git/mcp-tools domains exit 0.
This was referenced Sep 26, 2026
thisisjun786
marked this pull request as ready for review
September 26, 2026 13:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CRW-152 · crw-go-port wave 2 통합 브랜치입니다. store 트랙(#171)과 bridge 트랙(#170)을 합치고, 둘 다 필요한 todo 20을 얹었습니다. #170과 #171이 병합된 뒤에 병합되어야 합니다. wave 3 브랜치는 여기서 갈라집니다.
포함
internal/contracttest/registry.go(포팅된 도메인 목록의 합집합)와docs/port/inventory.md(두 fixture 행을 모두 두고, 합계는 측정값으로)에서만 났습니다.11c5bebc:crw relay doctor,store-identity,store-challenge,show,status가 Python과 같은 JSON을 냅니다. doctor는 실행 정책을 읽을 때internal/bridge/execution을 쓰기 때문에 두 트랙이 모두 필요합니다.검증
todo 20은 워커 → 독립 checker 2라운드(M4/M5/M12 뮤테이션 모두 잡힘) → 오케스트레이터 재실행 → 클린 카피 게이트 순서로 들어왔습니다. 클린 카피에서
uv sync --locked,make lint,make test,make contract, strictDomain/mcp-tools,check_inventory,check_test_map,check_corpus_count가 모두 exit 0이었습니다.뒤로 넘긴 것
show --message미리보기: todo 24/26--kind-module: todo 22Plan: .omo/plans/crw-go-port.md