Phase 5.0 of the CJS↔SDK hard-seam migration (parent #3524).
Foundational PR. Ships ONLY the synchronous primitive on the SDK
runtime bridge plus pinning tests. Per-family CJS router migrations
(state.*, verify.*, init.*, phase.*, phases.*, validate.*,
roadmap.*, frontmatter.*, config.*) become follow-up enhancements
that each reuse this primitive.
## What landed
- sdk/src/runtime-bridge-sync/index.ts (155 lines) — public API.
Exports executeForCjs(input: RuntimeBridgeExecuteInput):
RuntimeBridgeSyncResult. Synchronous; lazily creates the
synckit sync function on first call.
- sdk/src/runtime-bridge-sync/worker.ts (167 lines) — synckit
worker. Constructs a native-only QueryRuntimeBridge (with
allowFallbackToSubprocess: false), awaits its async execute,
catches GSDToolsError / GSDError, maps classification to the
six ADR-0001 canonical error kinds plus exit code.
- sdk/src/runtime-bridge-sync/index.test.ts (197 lines, 8 vitest
pinning fixtures) — success path; unknown_command;
native_failure (shape); validation_error (shape); shape
invariants; idempotency.
- tests/runtime-bridge-sync-smoke.test.cjs (99 lines, 4
node:test cases) — proves the primitive works from CJS
callers via require().
## Decisions
1. Synchronous-bridging mechanism: synckit. Disqualified:
- deasync: stagnant (68 open issues, single maintainer,
last release Nov 2025), private Node API (process.binding('uv')),
untested on Node 22, documented deadlocks with modern
Promise chains.
- Sync-native SDK refactor: technically infeasible —
acquireStateLock in state-mutation.ts uses await setTimeout
for retry backoff; making that fully sync requires either
Atomics.wait (which IS synckit), busy-loop (degrades
responsiveness), or breaking 100+ SDK consumers.
Synckit (v0.11.12) is pure JS, actively maintained (last
push today), stable public APIs (Atomics.wait +
SharedArrayBuffer), Node 22 compatible, no native compile.
2. Native-only transport inside the worker. The sync bridge
uses allowFallbackToSubprocess: false. Unknown commands
surface as unknown_command instead of spawning gsd-sdk.
Keeps the worker self-contained and predictable.
3. Worker path resolution. resolveWorkerPath() navigates ../..
from the loaded module URL to land at
dist/runtime-bridge-sync/worker.js — works under both
vitest (loads src) and CJS consumers (load dist).
4. GSDError.Blocked → validation_error. ADR-0001's 6-kind
taxonomy has no `blocked` kind; Blocked classification is
mapped onto validation_error since the operational shape
matches (prerequisite missing).
## Numbers
- 8 SDK vitest pinning tests pass.
- 4 CJS smoke tests pass.
- Full suite: 9286/9286 pass (baseline 9282; +4 from the
new smoke test cases).
- SDK vitest unit: 1860/1860 pass.
- Performance: 80ms first-call cold latency (Worker startup +
bridge construction); 0.1ms steady-state per-call latency
(10-call average after warmup). Well within budget for CJS
dispatcher overhead.
## Canonical error kind coverage
- unknown_command: covered with pinning fixture
- native_failure: shape coverage (handler that throws)
- validation_error: shape coverage (GSDError.Validation +
GSDError.Blocked)
- internal_error: shape coverage only (eliciting TypeError
reliably from a registered handler requires elaborate fixture)
- native_timeout: NOT pinned (no registered handler genuinely
times out; classification logic present in worker)
- fallback_failure: NOT pinned (subprocess fallback disabled
by design in sync bridge)
The classification logic is in the worker regardless; per-family
migration PRs will exercise the unpinned kinds incidentally.
## Wiring
- sdk/package.json: synckit ^0.11.12 added as runtime
dependency (not devDependency — it's required at runtime
whenever a CJS caller invokes executeForCjs).
- sdk/package-lock.json: regenerated.
- CONTEXT.md: new "Sync Runtime Bridge Module" entry added
after Dispatch Policy Module. Existing "CJS Command Router
Adapter Module" entry amended with one sentence pointing at
the primitive and the per-family migration roadmap.
No generator/freshness check needed for this phase — the
primitive IS the SDK (not a generated CJS mirror).
Closes#3555.