Summary
Companion sketch to cbcl-router PR #4 (SPEC-009 dialect distribution). Lands the client-side surface so a connected agent can publish dialects, discover them, and accept push announcements from the router — with R1–R5 enforcement on every install and auto re-hello when a new dialect lands.
Two commits on this branch for review clarity:
af9e116—chore: finish the danglingcbcl-router-client→harkrename in docs and test imports. No behaviour change; pulled in so the SPEC-009 commit stays focused.2d0e795—feat(SPEC-009): the protocol work.
Wire protocol (canonical meta grammar)
Three new frame builders in src/router.rs mirror the meta grammar cbcl-rs already defines — no router-namespaced verbs:
build_meta_teach_frame(define)// (meta (teach @router <define>))
build_meta_query_frame(name)// (meta (query (speak? <name>)))
build_meta_subscribe_frame(pattern)// (meta (subscribe (speak? <pattern>)))
R1–R5 enforcement on install (non-negotiable)
src/dialect_cache.rs is the new DialectCache. Public install path is try_install/2, which runs cbcl_parser::run_pipeline on (meta <define>) before the body enters the cache. R1 (no recursion), R2 (resource bounds), R3 (core preservation), R4 (sigs), R5 (shape) are all checked end-to-end via the upstream pipeline; a rejected push is logged, not cached, but still forwarded to the consumer so the user can see what was offered and investigate.
install_unchecked is #[doc(hidden)] and only used for tests that seed fixture bytes that aren't full R1–R5-clean defines.
Receive-loop wiring + auto re-hello
src/router.rs collapses the inbound binary/text arms into a single process_inbound/4 helper returning InboundOutcome::{Continue, Exit, Installed(name)}. classify_inbound (new in src/cbcl_validation.rs) recognises (meta (teach @<self> (define ...))) and routes pushes to try_install.
When try_install succeeds, the loop appends the new name to the session's advertised Vec and emits a fresh hello on the same socket. The cbcl-router same-pid re-hello path preserves in-flight counts and monitor refs (SPEC-009 register-flavour 2), so no work is lost. Stale-pid terminals are already rejected upstream by cbcl-router's current-session check.
The cache is per-agent today: one DialectCache lives inside each spawn_receive_loop, dying with the WebSocket process on disconnect. Daemon-wide sharing is a follow-up.
Hello + CLI cleanup
SPEC-009 collapses capability ≡ dialect on the router. Matching client-side baggage is gone:
build_hello_frameno longer emits:capabilities. Wire payload is now(lang cbcl-router (tell @router "hello" :agent-id ... :dialects (...))).InitArgsdrops--capability;--dialectis required.CreateAgentRequest,CreateAgentResponse,AgentStatus,AgentStatusSnapshot,AgentStore::validate_advertisement,insert_connected*,create_router_agent,CreatedRouterAgent— capabilities field removed.AgentError: droppedMissingCapability/DuplicateCapability/InvalidCapability; addedMissingDialect.hark daemon statusoutput no longer printscapabilities=[...].
New CLI subcommands
src/cli.rs adds hark dialect publish | query | subscribe. Handlers build the canonical frame and print to stdout — pipeable into hark reply today. Routing them through local_api → daemon → router so hark dialect query foo returns the teach-back payload directly is the next slice.
Tests
- 89 lib unit tests (incl. 12 new SPEC-009: dialect_cache R1–R5 enforcement, classify_inbound, meta frame builders)
- 31 integration tests across 6 suites (agent_workflow_cli, daemon_lifecycle, discovery, e2e_mvp, local_api, router_integration) — all touch points updated for the dialects-only API.
cargo test → 120/120 passing.
Test plan
cargo build --testscargo test— 120/120 across 9 suites- Manual smoke: run hark against the SPEC-009 cbcl-router branch, verify push install + auto re-hello over real WebSocket
- Wire
hark dialect querythrough local_api → daemon → router for inline round-trip
Known follow-ups (deferred)
- Daemon-wide dialect cache — cache is per-agent today; sharing across sessions in the same daemon is the natural next slice.
- CLI round-trip wiring — subcommands print frames; next slice routes through
local_apiso they actually call the router. - Client-side digest verification of teach-backs — the cache hashes locally; matching against the digest the router supplies in its teach reply (when present) gives end-to-end content-addressed identity.
- Client-side SPEC-009 companion spec — the trust model, cache eviction policy, and digest verification deserve their own short spec in
specs/.