feat(connect-cpp): add the C++17 convenience layer and surface the C/C++ channel - #107
Conversation
README twins: name C and C++ in the Connect capability bullet, add a tag-checkout passage with both headers and the two committed carriers, and replace the four-publish-channels sentence with the registry-backed / git-based classification. Docs home twins: name C/C++ in the how-to feature copy and add the dedicated C++ how-to link beside the TypeScript / native shortcuts. Quick-start twins: add the "C and C++ (git)" subsection under Connect with the tag clone, the header and carrier paths, and the committed platform sentence.
…T and docs Replace the four/five channel counts with the registry-backed / git-based grouping everywhere the connect native-binding channels are described, and record the C/C++ channel in the internal SSOT. - connect-publish-strategy.md carries the authoritative classification statement plus the registry-backed and git-based channel tables, and gains C/C++ rows in the surface inventory, staging, owner class, auth, disposition, non-goals, roadmap pointer and links. - spoke-connect.md Path B embedding row no longer describes only the uniffi route and lists the C/C++ committed headers and carriers. - AGENTS.md, CONCEPTS.md and the publishing knowledge doc/index row use the same enumeration and meaning. - connect-native-bindings how-to (EN + CN) states the classification, adds .hpp to the C/C++ row and gains a C and C++ - git subsection with the platform sentence and a link to the full walkthrough. - explanation/connect twins note the C ABI + C++17 convenience relationship and the callback-transport pointer.
…list The C and C++ subsection listed the win-x64 DLL but omitted the committed Rust-produced import library the Windows link step consumes. List crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll.lib in both the EN page and its zh twin so the page's acquisition paths cover the two headers and both carried Windows artifacts. Fixes review finding T2-2 on docs/packages/quick-start.md and docs/zh/packages/quick-start.md.
…s and loopback `bindings/cpp/include/spoke_connect.hpp` is a header-only C++17 layer over the hand-written C ABI in namespace `spoke::connect`: a move-only `Buffer` and `Error`/`Result<T>`/`Result<void>`, the eight core free functions, the three core session objects (`NonceStore`, `OutboundSequence`, `InboundSequence`) and the loopback pair and ends, over a reusable move-only handle base. Every production call reaches only an existing declaration in `spoke_connect.h`; the carrier and the ABI revision 1 stay untouched. The smoke gains a second translation unit (`Smoke/convenience.cpp`) that shares `Smoke/support.hpp`'s golden read and parse, so both units include the convenience header and one linked program covers single-header ODR. `cpp-smoke.mjs` compiles and links both units and requires the new ordered banner before the final one; the `cpp-symbol-check.mjs` C++ probe now includes `.h` and `.hpp` (twice) and instantiates `Result`, `Buffer` and a move-only handle, so the second header cannot stop compiling behind a green C-only check.
The plan's frozen consumer configuration (A2) fixes MSVC as `/std:c++17 /EHs-c- /GR- /MD /D_HAS_EXCEPTIONS=0`; the smoke runner compiles on behalf of a consumer, so its Windows argv must carry the whole configuration. The clang argv already matched A2 and is unchanged.
`spoke_connect.hpp` gains the host-callback side of the C ABI: the three callback records (`TransportCallbacks`, `PortsCallbacks` with all thirteen slots named as the C table, `ToolCallbacks`), their complete static thunk tables, the `Transport` / `PortsHandler` / `ToolHandler` factories — which clear the caller's `unique_ptr` only after the C constructor succeeds — and the `RemoteAdapter` / `ConnectResponder` wrappers over the frozen connection and serving methods: baseline ports, `project` / `compute` / `list_fork_timeline_events` / `extract`, `invoke_tool`, `register_tool_handler`, the state / session / manifest accessors and explicit `close`. No method exists that the production C surface does not have; the C header, the carrier, the committed natives and ABI revision 1 are untouched. The bridges follow the frozen error contract: a non-empty result moves into host-owned storage released exactly once, a zero-length result uses a zero record (the carrier releases only populated buffers), a present empty diagnostic takes the address of a static empty string, and packaging several error fields holds them in local RAII until the record is handed over. Exception-enabled builds contain an escaping host exception — from the callback and from the packaging that follows it — into `SPOKE_CONNECT_TRANSPORT_IO` or `SPOKE_CONNECT_FFI_REJECTED` with `INTERNAL_ERROR` / `callback` from static literals; with exceptions disabled the header contains no `try`, `catch` or `throw` at all, and the same calls report failure through `Result`. `Smoke/convenience.cpp` proves the group over a host queue transport: `serve` → `connect` → `Established`, a real baseline ports round trip with the absent-versus-present-zero base revision, a tool round trip in both directions plus the reverse registration path, a refusal preserving `code`, `message`, `kind` and `wire_code`, a present empty `kind` that stays present, a refused factory that leaves the caller its context, a transport handle released after the session cloned it, and every callback record released exactly once after close. `cpp-smoke.mjs` builds each RID twice — exceptions disabled (the consumer default) and exceptions enabled — each against its own ordered banner list, with the enabled build additionally reporting the containment row. `cpp-symbol-check.mjs` runs its probes through a build step the negative controls reuse, adds the `.hpp` exception-syntax mutation over a complete copy of the header tree (so an include-path mistake cannot pass as a control), and its C++ probe now instantiates the callback records, the three factories and the two session wrappers.
The banner verifier scanned for each expected banner in order and recorded only the ones it could not find, so a run that printed extra or repeated banners still passed: the exceptions-disabled build could report the enabled configuration's containment banner, or a group could run twice, and the gate stayed green. Extract every banner-shaped line the run printed and require that sequence to equal the configuration's expected sequence — same count, same order, same text — failing with the first differing position when a banner is missing, extra, repeated or reordered.
…+ coverage Wrap the router group of the frozen C surface in `spoke::connect`: a move-only `MultiPeerRouter` with `create`, register/unregister/list peers, the composed manifest, the routed baseline ports and `invoke_tool`. The production C surface has no router extract/project/compute/fork member, so none is invented, and registration borrows the adapter — the destructor releases the router's own references and never closes a caller-owned adapter. The loopback ends stay independent helpers; no end is presented as a callback `Transport`. Prove it in the smoke with a live session: register an established adapter, route a real tool call through the router, unregister the peer and observe the terminal `no_capable_peer` reject with zero wire traffic, then show the released router leaving the caller-owned adapter Established and serving. The group prints `C++ convenience router: PASS` before `C++ smoke: PASS` in both configurations. Close out the coverage statement: `parity.md` gains a C++ counterpart column for every production entry — business API versus RAII mechanics — and the symbol gate's C++ probe now instantiates the value layer, all eleven opaque handles and the three callback factories under the consumer no-exception flags.
…le walkthrough The C++ page leads with `spoke_connect.hpp`: three consecutive blocks compose one complete `host.cpp` (host queue transport, demo identity with a served echo tool, dial plus one real invoke) that builds and runs against the committed carrier, followed by the `Buffer`/`Result`/close semantics and where a real network transport replaces the callbacks. The acquisition table, Windows no-exception flags, smoke banners (two configurations per RID) and the symbol gate's inclusion-probe label are synced to the shipped artifacts. The tracked specs record the layer as a current fact: coverage, values, the result and exception contract, callback bridges and explicit close in `connect-cpp-binding.md`, with `.hpp` folded into the header gate and the convenience exclusions removed; the channels spec drops the convenience non-goal and states the builder's closed two-target set. The binding, UE and crate READMEs follow, with the golden-vector sentence no longer counting bindings.
The crate README (published on crates.io) carried five `../../.mstar/specs/` pointers that do not resolve for its audience and expose the internal agent-facing SSOT. Route the wire facts to the on-site Connect wire reference (page and section anchors) instead. The C++ binding and UE READMEs each pointed at the internal C++ binding decision record; the consumer how-to and the C++ binding README already carry those facts, so drop the decision-record references and keep the sentences. No technical content changes; link destinations only.
The Windows lane invoked cpp-symbol-check.mjs without --self-test, so the four negative mutations (missing declaration, invented declaration, callback-signature drift, .hpp exception-syntax probe) never ran on MSVC even though the plan's B2 gate and the documented delivery recipe include the flag on both platforms. The self-test only reuses tooling the step already has (cl.exe and dumpbin) and adds compile time, so the flag belongs on this lane too.
…se path The composed `host.cpp` terminated through `std::exit` on failure paths that follow a successful `ConnectResponder::serve()` — the tool handler, the tool registration and the dialer transport — and on a failed dial in `main`. Those paths bypassed both automatic-object destruction and the explicit `RemoteAdapter::close()` / `ConnectResponder::close()` / host-queue-close sequence the same page documents, which the plan's C4 freeze requires a post-establish failure to use. `start_endpoints` now returns `Result<Endpoints>` and routes every step after `serve` through one `close_ordered` helper that ends the sessions in the documented order and closes the host queues before the failure travels back to `main`; a failed dial and a failed `RemoteAdapter::state()` use the same helper, which the success tail now also calls. `unwrap` stays for the steps that run before a serving handle exists; its `Result<void>` overload became unused and is removed. The happy path and its stdout are unchanged: the page's own macOS command builds and runs the concatenated blocks and observes the same `state: Established` line, session id and echo JSON. EN and CN keep the C++ source byte-identical.
…p-consumer-surface
There was a problem hiding this comment.
Stale comment
Risk: high. Left a non-blocking comment; one reviewer is already assigned. This C++ FFI convenience layer (transport and callback bridges) is above the medium approval threshold, so it needs human review. Cursor Bugbot and Cursor Security Agent were not present after the first check poll.
Sent by Cursor Approval Agent: Pull Request Router and Approver
|
`bannerLines` split the child's output on "\n" and tested every line against /^.+: PASS$/. The Windows smoke child writes CRLF, so each line kept a trailing "\r" and nothing matched: the windows-smoke job failed reporting 9 expected, 0 printed, from a run that had printed all nine banners and passed every assertion. Splitting on /\r?\n/ consumes CRLF and LF alike as a single line terminator, so the extraction no longer depends on the platform's line ending. The check itself is unchanged: the observed banners must still equal the configuration's expected list exactly — same count, same order, same text — and the diagnostic still names the first differing position with both counts.
There was a problem hiding this comment.
Stale comment
Risk: high. Left a non-blocking comment; one reviewer is already assigned. This C++ FFI convenience layer with transport and callback bridges is above the medium approval threshold, so it needs human review. Cursor Bugbot and Cursor Security Agent were not present after the first check poll.
Sent by Cursor Approval Agent: Pull Request Router and Approver
…ed in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
There was a problem hiding this comment.
Stale comment
Risk: high. Left a non-blocking comment; one reviewer is already assigned. This C++ FFI convenience layer with transport and callback bridges is above the medium approval threshold, so it needs human review. Cursor Bugbot and Cursor Security Agent were not present after the first check poll.
Sent by Cursor Approval Agent: Pull Request Router and Approver
…feat/cpp-consumer-surface
…helper Copilot Autofix 4beb3d6 added resolve_fixture_path() to Smoke/main.cpp and spelled the report label unqualified; kFixtureLabel is declared in the spoke_smoke namespace by Smoke/support.hpp, so the three references did not compile. Import it with a using-declaration beside the file's other spoke_smoke imports (banner, check, fail, Golden, json_string_field, load_golden, read_file), matching how this translation unit already refers to that namespace. The helper's behaviour is unchanged: the fixture path is still canonicalised and confined to crates/spoke-connect/tests/fixtures.
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
There was a problem hiding this comment.
Stale comment
Risk: high. Left a non-blocking comment; reviewers were not newly assigned because one reviewer is already requested. This C++ FFI convenience layer with transport and callback bridges is above the medium approval threshold, so it needs human review.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
Risk: high. Left a non-blocking comment; reviewers were not newly assigned because one reviewer is already requested. This C++ FFI convenience layer with transport and callback bridges is above the medium approval threshold, so it needs human review.
Sent by Cursor Approval Agent: Pull Request Router and Approver


What
Adds the C++17 consumer surface for the Connect C/C++ channel, and makes that channel discoverable from the
entry documentation a C++ integrator actually reads.
Convenience layer —
crates/spoke-connect/bindings/cpp/include/spoke_connect.hpp: a hand-written,header-only C++17 wrapper over the existing C ABI (
spoke_connect.h, ABI revision1unchanged). Move-onlyowning types for the C resources, a
Result<T>/Errorerror channel,std::string_viewtext inputs with anexplicit owned copy,
std::optionalfor the C optional-value convention, callback records built fromstd::functionand transferred throughunique_ptronly after the C constructor succeeds, explicitclose()on sessions (destruction frees but never closes), and an exception-containment branch selected from the compiler
exception macros. No new exported symbol, no record-layout change, no new artifact or version-bearing manifest.
Verification — the smoke runner compiles and runs two configurations per RID (exceptions disabled and
enabled) and requires each configuration's assertion banners to match its expected list exactly, so a
banner leaked from the other configuration fails the run; the C ABI symbol/layout/callback gate's C++ probe now
also compiles the wrapper and gained a
.hppexception-syntax negative control; the Windows CI lane runs thesame gate with
--self-testso the negative mutations execute on MSVC too.Documentation — the C++ how-to pages (EN + CN) carry a runnable walkthrough composed of three copyable
blocks that reaches
Established, invokes a tool and closes in the documented order on both the success and thefailure paths; the C/C++ channel is surfaced on the root READMEs, the docs home and the package quick-start; one
authoritative registry-backed / git-based channel classification replaced the three drifting counts across
CONCEPTS.md,AGENTS.mdand the specs; and the two tracked C++-related specs state the shipped facts.Verification
node tooling/connect/cpp-symbol-check.mjs --header … --library … --self-test→83 declarations, 83 exports, 0 missing, 0 extra; layout, callback-signature, C and C++ probes green; all four negative mutations failingas intended
node tooling/connect/cpp-smoke.mjs --rid osx-arm64→ both configurations green with their exact orderedbanners
node tooling/docs/twin-parity.mjs→ every page twinned, heading structures matchingpnpm docs:buildgreen;node tooling/docs/deadlink-check.mjs→ 49 pages, 2 761 anchors, 0 deadconfigurations, observing
Established, a non-empty session id and the echo JSONApprove with residuals; QA gatemandatory→Approve, 10/10 acceptance criteria metResiduals
low) — the symbol gate's C++ inclusion probe covers only the no-exceptions configuration; theexception-enabled path is covered by the smoke runner. Tracking: project register
entries["cpp-consumer-surface"].low) — the committed C++ carrier provenance records a header hash forspoke_connect.honly; aspoke_connect.hppfingerprint can only land at the next carrier refresh. Tracking: the same register bucket.No blocking finding and no
criticalremains open.Notes
osx-arm64andwin-x64are unchanged; the Windows lane's run is CI-owned and wasnot executed locally.
linux-x64C carrier is recorded as a deferred roadmap item with owner, trigger and done definition; it isnot implemented here.
this change owns was repointed to public destinations.