Agents still write the code. Synapse gives them current team context before they edit, then records contract-level changes after they edit, so other agents can avoid collisions.
When several teammates each point a coding agent (Claude Code, Cursor, Copilot, Gemini…) at the same repo, they collide — two agents change the same function's signature in incompatible ways and nobody notices until merge. Synapse is a realtime layer that catches this the moment it matters: it warns an agent before it edits a symbol a teammate's agent just touched.
⚠ Synapse: 1 potential conflict(s) before editing src/auth/token.ts
• [same_symbol_unpushed] alice has an unpushed change to validate (with alice)
→ coordinate before editing
- Contract-level, not file-level — compares real
before→aftersignatures and classifies them breaking / compatible / identical / divergent, not just "same file touched." - Deterministic first — detection is never the LLM. An optional OpenRouter layer only enriches analysis and resolution; it can raise, never downgrade, a verdict.
- Any agent — Claude Code via
PreToolUse/PostToolUsehooks, every other agent via MCP, with the same check-before-edit / report-after-edit behavior. - Warns, never blocks — agents query, humans decide. Synapse surfaces the conflict inline and gets out of the way.
flowchart LR
subgraph m1["Alice's machine"]
A["Claude Code / Cursor"] -->|"report after edit"| AD["synapse daemon"]
end
subgraph m2["Bob's machine"]
BD["synapse daemon"] -->|"⚠ warn before edit"| B["Claude Code / Cursor"]
end
AD <-->|"contracts only — signatures, never code"| S[("Synapse server<br/>fanout + durable state")]
S <--> BD
Each daemon keeps your raw code local — only symbol-level contracts (signatures, never function bodies) ever cross the wire.
npm install -g @kumario/synapse # installs the `synapse` binaryPrerequisites: Node.js 20.19.0+ and npm 11.4.1. Python 3.10+ and Go 1.22+ are optional — needed only to analyze .py / .go files; without them, those languages degrade gracefully to file-level detection.
Two machines coordinate when their daemons share the same repoId (auto-derived from the git remote) against the same server.
# Host — start the server, expose it over a public wss:// tunnel,
# write the URL into .synapse/team.json, and print the teammate command + token.
synapse up --serve --tunnel
# Teammate — commit .synapse/team.json, share the token out-of-band, then:
SYNAPSE_AUTH_TOKEN=<token> synapse upThen restart Claude Code in the repo so it loads the freshly installed hooks, and run synapse doctor to confirm the room — it should list your teammate as a peer (resolved identity, server reachability, auth vs. unreachable, protocol version, live peers). Now when Alice's agent edits validate, Bob's agent sees the warning above before it touches the same symbol.
Want to see it first, solo? Run
synapse demo— it spins up a server and two daemons in a throwaway sandbox (own SQLite state, own free ports, a randomdemo/<hex>room — never your repo), narrates Alice changing a return type and Bob's next check catching the conflict, then tears everything down. Pass--keepto leave the sandbox on disk, or--jsonfor a machine-readable result.
Watch a real conflict — the two-agent walkthrough
Use the team setup above to bring up the host (synapse up --serve --tunnel) and teammate (SYNAPSE_AUTH_TOKEN=<token> synapse up). Then restart Claude Code in the repo, confirm the room with synapse doctor (it should list the other person as a peer), and drive it in order:
- Alice asks her Claude: "Edit
src/auth/token.tssovalidatereturnsToken | null." Let it save — the PostToolUse hook reports the delta. - Bob asks his Claude: "Edit
src/auth/token.tssovalidatereturnsPromise<boolean>." Before it writes, Bob's PreToolUse hook surfaces "⚠ Synapse: alice has an unpushed change tovalidate— coordinate before editing," and Claude asks Bob how to proceed.
Gotchas — why a demo can look like nothing happened
- Share a real token.
--tunnelrequires auth; withoutSYNAPSE_AUTH_TOKENa random token is generated and printed only once. Pass your own so the teammate can join.synapse doctorshowstoken=unset → 401when this is wrong. - Different sessions only. A session never warns about its own change — editing twice from one machine/session shows nothing.
- Order matters. The editor must save first (PostToolUse reports) before the other agent's PreToolUse check can see it.
- Restart Claude Code after
synapse upso it loads the freshly installed hooks. Don't commit.claude/settings.json— the hook path is machine-specific; each person'ssynapse upwrites their own.
Local dry-run — two agents, one machine
Two daemons against one local server, driven by the CLI. Proves the whole detect loop.
# 1. A throwaway project with a symbol to fight over
mkdir -p /tmp/synapse-demo/src && cd /tmp/synapse-demo && git init -q
printf 'export function area(w: number, h: number): number {\n return w * h;\n}\n' > src/widget.ts
git add -A && git -c user.email=demo@local -c user.name=demo commit -qm init
# 2. Terminal 1 — server + Alice's daemon
synapse up --serve --member alice --session alice --port 4011 --repo-id demo/playground
# 3. Terminal 2 — Bob's daemon against the same server
synapse daemon --member bob --session bob --port 4012 --server ws://localhost:4010 --repo-id demo/playground
# 4. Terminal 3 — Alice records a change, Bob checks the same symbol first
synapse report --port 4011 --file src/widget.ts --symbol ts:src/widget.ts#area --summary "area() now takes a Rect"
synapse check --port 4012 --file src/widget.ts --symbol ts:src/widget.ts#area
# → verdict: "warn", rule: "same_symbol_unpushed", counterpart: "alice"
synapse whatsup --port 4012Network & filesystem safety
By default, local Synapse daemons and servers bind only to loopback (127.0.0.1). Set SYNAPSE_DAEMON_HOST or SYNAPSE_SERVER_HOST explicitly when you intentionally need a LAN/public listener, for example SYNAPSE_SERVER_HOST=0.0.0.0 in a container or VM.
All local file arguments are resolved inside the configured worktreeRoot. Absolute paths, .. traversal, and symlink escapes are rejected before the daemon reads a file or hands it to an analyzer.
apps/
cli/ local daemon, CLI commands, and MCP stdio adapter
server/ websocket fanout server + durable StateStore (SQLite)
packages/
analyzer-ts/ TypeScript contract extraction
analyzer-py/ Python contract extraction + dependency graph
analyzer-go/ Go contract extraction + dependency graph (go/parser sidecar)
(tree-sitter + jedi sidecar over JSON-RPC/stdio)
protocol/ shared wire, state, and symbol types
conflict-engine/ pure conflict evaluator
The server is single-process with an in-memory hot path backed by a durable store. The daemon keeps raw code local. Detection is deterministic; humans decide — Synapse warns inline, never auto-blocks.
| Layer | Without a key | With OPENROUTER_API_KEY |
|---|---|---|
| Detection | Fully deterministic | Unchanged (never affected) |
| Analysis | Structured before→after verdict | Task-aware prose; can raise but not downgrade a verdict |
| Contract resolver | contract_divergent → escalate; same_symbol_unpushed → adopt counterpart |
Synthesizes one merged contract (must parse via the real analyzer) |
| Mediator resolution | Deterministic proposal class/status/signatures/call-sites | Same proposal semantics; optional LLM-authored adapt prose when resolving |
| Session summary | Structured list of changes | 2–3 prose sentences |
Set the key in .env (see .env.example). Model defaults to anthropic/claude-haiku-4.5, overridable via SYNAPSE_LLM_MODEL. Disable layers independently with SYNAPSE_LLM_EXPLAIN=0, SYNAPSE_LLM_RESOLVE=0 (contract resolver and mediator adapt prose), SYNAPSE_LLM_SUMMARY=0.
An analysis's actions[] may carry a command suggesting a Synapse tool to call next (e.g. synapse_whatsup, synapse_why), rendered as → run: ... in the Claude Code hook output. The deterministic floor attaches these for its own rule-appropriate suggestions even without a key; with a key, the model may also suggest one, validated against the same fixed allowlist (packages/protocol/src/command-catalog.ts) — an unknown tool is dropped but the step text is kept. Set SYNAPSE_LLM_COMMANDS=0 to drop the command catalog from the LLM prompt (the allowlist check still runs).
When two live agents contest the same symbol, Synapse classifies the collision deterministically. A mechanical conflict means one side changed the contract, or both sides reported identical resulting signatures; Synapse creates the existing suggest-only keep/adapt proposal, delivered in state.snapshot, and it becomes resolved only after both agents accept with resolution.ack. A semantic conflict means both sides reported mutually exclusive signatures; Synapse does not fabricate a merged after. It broadcasts an awaiting_owner proposal with the two candidate session IDs, and the Owner chooses the winner through POST /auth/projects/resolve-winner?repoId=<id>&proposalId=<id>&winnerSessionId=<id>, cookie-authed and authorized by Project ownership. After the Owner picks, the winner keeps its signature and the other side gets the deterministic adapt call-site list, then the normal accept/reject/timeout flow applies. When configured, the mediator LLM may rephrase only the losing side's adapt guidance; it cannot choose winners, change after, alter detection verdicts, change proposal status, or edit code. A reject from either agent, or a TTL timeout while resolving, voids the coordinated pair (status: "voided" with voidReason: "rejected" or "timeout"); the TTL is SYNAPSE_RESOLUTION_TTL_MS (default 5 minutes) and starts only after a proposal is resolving. See ADR-0002.
The CLI binary is synapse (apps/cli/src/index.ts). In a dev checkout, run any command via npm run dev --workspace @synapse/cli -- <command>.
| Command | Description |
|---|---|
daemon |
Start the local daemon |
check |
Call the local synapse_check endpoint |
report |
Call the local synapse_report endpoint |
push |
Notify Synapse that files were pushed |
feedback |
Record acted/dismissed feedback for a conflict warning |
insights |
Show local aggregate coordination insights, including mediator proposal counts |
session |
Start, heartbeat, or end a local session |
whatsup |
Show the daemon's current team-state briefing |
why |
Search Synapse memory with source citations |
pr-brief |
Local PR handoff briefing for a base/head branch pair |
mcp |
Run a stdio MCP server exposing Synapse tools plus read-only context resources |
connect |
Wire other agents (Cursor, VS Code/Copilot, Gemini CLI, Windsurf, any MCP client) to the MCP server |
join |
Write .synapse/config.json, install Claude Code hooks, and connect other agents |
up |
join + preflight + start daemon (--serve / --tunnel for the host) |
keygen |
Mint a project-scoped key for this repo (needs SYNAPSE_MASTER_SECRET) |
doctor |
Preflight: identity, server reachability, auth, and live peers |
hook |
Claude Code hook entrypoint (pre|post); reads hook JSON on stdin |
analyze |
Extract TypeScript contract symbols from a file |
help |
Print usage and examples |
Claude Code gets PreToolUse / PostToolUse / SessionStart hooks that fire synapse_check before edits, synapse_report after edits, and a synapse_whatsup catch-up at session start. A file-based pre-check records the current contract snapshot locally, so the first post-edit report can emit the real before -> after delta without requiring a separate baseline call. Every other agent gets the same behavior through MCP — synapse join (and synapse connect) sets it up automatically:
synapse connect # wire up every supported agent
synapse connect --agent cursor,vscode # or just the ones you useThis does three things so other agents connect seamlessly and then use Synapse the way it's intended:
- Registers the stdio MCP server in each client's own config —
.cursor/mcp.json,.vscode/mcp.json,.gemini/settings.json, and the cross-agent.mcp.json— pointing atsynapse mcp. The adapter resolves its room (repoId, session, daemon port) from.synapse/config.json, so there is nothing else to configure. - Exposes MCP resources for passive context —
synapse://briefing,synapse://team-state,synapse://decisions, andsynapse://pr-brief— so clients can list/read the current team digest, cited memories, and PR handoff context without making a tool call. Tools remain the action surface for checks, reports, pushes, feedback, aggregate insights, PR briefings, and argument-specific memory searches. - Drops rules files that encode the hooks as instructions —
AGENTS.md,.cursor/rules/synapse.mdc, and.windsurf/rules/synapse.md— telling the agent to read context resources when available, callsynapse_checkbefore editing,synapse_reportafter, and usesynapse_whatsupas the fallback session-start catch-up. The MCP server also advertises the same guidance via the protocol-nativeinstructionsfield, so even clients that ignore rules files still receive it. The generated command reference comes frompackages/protocol/src/command-catalog.ts, the same catalog that grounds deterministic and optional-LLM action suggestions.
Every write is idempotent and preserves your existing content (managed blocks for markdown, key-merge for JSON), so it is safe to re-run.
Resolved at server startup. /health and the GitHub webhook (its own HMAC) stay open; credentials are sent via ?token= / Authorization: Bearer and compared in constant time — never written to disk.
| Mode | Trigger | Behavior |
|---|---|---|
| open | neither var set | No auth — keeps local/dev and verify scripts hermetic |
| shared-token | SYNAPSE_AUTH_TOKEN |
Any valid token reads/writes any project |
| project-key | SYNAPSE_MASTER_SECRET |
Real tenancy: key = base64url(HMAC-SHA256(secret, repoId)), authorizes only its project (checked at handshake + per-message) |
For hosted Synapse, register a GitHub App and set SYNAPSE_GITHUB_APP_ID, SYNAPSE_GITHUB_APP_CLIENT_ID, SYNAPSE_GITHUB_APP_CLIENT_SECRET, SYNAPSE_GITHUB_APP_PRIVATE_KEY, and SYNAPSE_GITHUB_WEBHOOK_SECRET on the server.
GitHub console settings:
- User authorization callback URL:
https://<host>/auth/github/callback - Setup/installation callback URL:
https://<host>/auth/github/setup - Webhook URL:
https://<host>/webhooks/github - Webhook secret: the same value as
SYNAPSE_GITHUB_WEBHOOK_SECRET - Repository permissions: Contents read-only, Metadata read-only, Pull requests read-only
- Subscribe to events: Push, Pull request, Pull request review, Issue comment
Sign-in and repo-claiming (below) are both live. For claiming, enable "Request user authorization (OAuth) during installation" on the App so the setup callback carries an OAuth code, and set the optional SYNAPSE_GITHUB_APP_SLUG (the App's URL slug) so the server can build the install URL. Setting only SYNAPSE_GITHUB_WEBHOOK_SECRET remains valid for signed webhooks without the full App auth flow.
Credentials are sent via Authorization: Bearer (the server still accepts ?token= for back-compat), keeping tokens out of URL query strings and access logs.
The first human trust boundary. Active only when the GitHub App env above is fully configured (visible as githubApp: "configured" on /health); otherwise the routes 404 and the web app stays signed-out. Four same-origin routes:
| Route | Purpose |
|---|---|
GET /auth/github |
Start the user-to-server OAuth flow (sets a short-lived signed state cookie for CSRF, redirects to GitHub) |
GET /auth/github/callback |
Exchange the code, upsert the Owner on first login, set the session cookie, redirect to / |
GET /auth/me |
Return the signed-in Owner ({ owner: { login, name, avatarUrl } }) or 401 |
POST /auth/logout |
Clear the session cookie (GET accepted too) |
The session is a stateless signed cookie (synapse_session, HttpOnly; SameSite=Lax, 30-day HMAC; Secure when the public origin is https) — no sessions table, so sign-out just clears the cookie. The HMAC key is derived from the OAuth client secret, so no extra env var is needed. Set SYNAPSE_PUBLIC_URL (optional) to build the OAuth redirect_uri; it defaults to http://<host>:<port>.
This cookie session is identity only — it is never a daemon credential and never authorizes a WS room or /state, which keep using the separate machine project-key/shared-token path. A small users table (same backend selection as the state store) holds Owner identity; the GitHub user access token is not persisted.
Turns a signed-in Owner into the Owner of a Project. Claiming runs the Owner through the GitHub App installation flow for a repo they can push to, then records ownership and mints that repo's project-key. It needs SYNAPSE_MASTER_SECRET set (for the key) and SYNAPSE_GITHUB_APP_SLUG set (for the install URL); absent either, /auth/projects/add returns 503 claiming_unavailable.
| Route | Purpose |
|---|---|
GET /auth/projects/add |
Start the claim: redirect the Owner to the App install page (/apps/<slug>/installations/new) with a short-lived signed state cookie |
GET /auth/github/setup |
Install setup callback: verify session + state, exchange the OAuth code for the user access token, list the installation's repos, and claim each one the user can push to |
GET /auth/projects |
Return the Owner's own claimed projects ({ projects: [{ repoId, projectKey }] }) |
The setup callback requires "Request user authorization during installation" enabled on the App, so it carries an OAuth code; if that setting is off, the code is absent and setup returns 400 missing_installation. The user access token is used once (to list the installation's repositories) and then discarded — never persisted. Repos the Owner cannot push to are never claimed.
The project-key is minted via deriveProjectKey(SYNAPSE_MASTER_SECRET, repoId) and is idempotent per (owner, repo): re-installing keeps the original key, so a running daemon's credential never changes. This key IS the per-repo daemon credential and is returned only to the authenticated Owner who claimed it (via GET /auth/projects); the cookie session never acts as a daemon credential. Installing the App also makes the repo's webhooks live automatically — GitHub delivers push/PR/review to the existing POST /webhooks/github, so no new webhook wiring is needed.
After claiming, the dashboard shows a per-Project onboarding panel that walks the Owner to a live daemon. It lists two steps: install the CLI with npm install -g @kumario/synapse, then start the daemon for that Project with SYNAPSE_PROJECT_KEY=<key> synapse up --server <wss-url> --repo-id <repoId>. The <key> is the same project-key minted at claim time — shown only to the owning Owner — and the <wss-url> is this hosted server's origin (https→wss). Each card polls the Room (GET /state?repoId=&token=<project-key>, the Owner's per-repo credential, not the cookie session) and flips its badge to Connected once a daemon joins. Dev-parity caveat: the panel is same-origin only — plain vite dev has no proxy to /auth/projects, so it stays signed-out (hidden) there, matching the rest of the auth surface.
A signed-in Owner sees a Your Projects panel listing every Project they have claimed, selects one, and watches its live Room — agent sessions, edit locks, contested symbols, mediator proposals, and the ship trail — reusing the same dashboard panels as the demo. The read is served by GET /auth/projects/state?repoId=<id>, which is cookie-authed and authorized by ownership: it returns 401 without a session, 403 for a repo the Owner has not claimed (ownership is checked via the project store, never via a token), and 400 without a repoId. This boundary is distinct from the machine GET /state (project-key) path — the two read paths stay separate. Live updates here are delivered by polling this cookie-authed route (~2s); the daemon's project-key WebSocket path is unchanged, and the WS handshake never accepts the cookie. Same-origin only, like the rest of the auth surface.
The Room dashboard's Resolution mediator card shows resolving, resolved, and escalated proposals (awaiting_owner or voided). Awaiting-owner semantic conflicts expose winner-choice buttons that call the existing cookie-authed POST /auth/projects/resolve-winner route; the dashboard never edits code or sends daemon WebSocket commands.
Kick a Session — an Owner can force-end a runaway or stale agent Session in a Project they own via POST /auth/projects/kick?repoId=<id>&sessionId=<id>. It is cookie-authed and authorized by ownership (the same project-store check as the read above): 401 without a session, 403 for a repo the Owner has not claimed, 400 without repoId/sessionId. The kick ends the Session (status: ended), releases its edit locks, closes its socket, and broadcasts the new Room state; a reconnecting daemon returns as a fresh Session — kick is an interrupt, not a ban (there is deliberately no blocklist). This is HTTP only — the kick is never a browser WebSocket message on the machine protocol; the browser stays off the daemon wire entirely.
State store — persisted per entity (sessions, locks, deltas, pushes, events, resolutions, summaries, feedback as rows; every mutation writes only its own row). Backend selection: SYNAPSE_DATABASE_URL → Postgres (the shared-database backend for multi-instance deployments; the pg driver loads only when selected); else SYNAPSE_DB_PATH → file-backed SQLite (WAL) that survives restarts; neither → ephemeral in-memory SQLite. Pre-existing SQLite snapshot databases migrate to per-entity rows automatically on first boot. Postgres schema initialization is serialized with advisory locks, and startup always attempts to release the lock before returning the pooled connection, including when DDL fails.
Multi-instance — set SYNAPSE_REDIS_URL (alongside a shared SYNAPSE_DATABASE_URL) to run several server instances behind a load balancer: after a mutation, the instance publishes the repo's Redis channel; the others re-read that repo from the shared store and re-broadcast the fresh snapshot to their local rooms. Redis carries no state — it is purely the wake-up signal (the redis driver loads only when selected), and lock/session expiry stays timestamp-based against the shared rows, so every instance evaluates the same liveness. Unset → the single-instance path, unchanged.
| Capability | Summary |
|---|---|
| Resilient channel | Exponential backoff + full jitter (SYNAPSE_RECONNECT_BASE_MS / SYNAPSE_RECONNECT_MAX_MS), capped offline outbox flushed in order on reconnect, and 20s server pings (SYNAPSE_WS_PING_INTERVAL_MS) that terminate half-open sockets |
| Observability | JSON logs gated by SYNAPSE_LOG_LEVEL (default info) + Prometheus counters at GET /metrics |
| Ingress validation | Every server-bound wire message is validated against shared zod schemas before any state mutation, and daemon-bound server frames are validated before they update the warm cache. WS/webhook bodies and local daemon JSON tool bodies are capped at 1MB; malformed local JSON returns 400 and oversized local JSON returns 413 |
| Rate limiting | Per-connection WS budget (SYNAPSE_RATE_LIMIT_PER_MIN, default 600) and a webhook budget (SYNAPSE_WEBHOOK_RATE_LIMIT_PER_MIN, default 120): over-limit messages are acked rate_limited and dropped before any mutation; webhooks answer 429. 0 disables |
| Webhook posture | A server running with auth (shared token or project keys) refuses unsigned webhooks with 403 until SYNAPSE_GITHUB_WEBHOOK_SECRET is set; open mode (local/dev) is unchanged |
| Protocol negotiation | Versions are exchanged at the WS handshake: legacy clients (no announcement) connect as v1, newer clients downgrade to the server's dialect, out-of-range clients are refused with HTTP 426 + the supported range in headers. Protocol v2 sockets receive incremental state.delta frames after mutations; v1 sockets continue to receive snapshots. /health reports protocolVersion + minProtocolVersion; synapse doctor fails loudly on non-overlapping ranges |
| Adaptive severity | synapse_feedback telemetry demotes a noisy rule (≥5 dismissals, ≥80% dismiss rate) from warn to info; detection never changes. Opt out with SYNAPSE_ADAPTIVE_SEVERITY=0 |
| Branch-aware severity | Cross-branch dependency_changed/stale_base conflicts demote warn → info (they bite at merge time, not on the next keystroke); merge-blocking rules (same_symbol_*, contract_divergent) never demote. Sessions/pushes carry their git branch (webhook pushes derive it from ref), refreshed on every heartbeat so a mid-session checkout propagates within ~30s; unknown branch → no change. Opt out with SYNAPSE_BRANCH_AWARE_SEVERITY=0 |
| File watcher | The daemon watches the worktree (same ignore set as the analyzer scan), so manual edits — no agent, no synapse_report — still emit contract deltas through the report path. Debounced per file (SYNAPSE_WATCH_DEBOUNCE_MS, default 400ms); only analyzable sources are reported. While the watcher is active, warm pre-edit checks reuse the cached dependency graph instead of re-scanning the source tree (any report or watched change invalidates it; synapse_graph_cache_hits_total counts reuse). Opt out with SYNAPSE_FILE_WATCHER=0 |
| GitHub webhook binding | Signed webhook events are bound to the payload's repository.full_name; local verifier overrides must use the same repo identity as the signed payload instead of remapping arbitrary repository names |
RAG memory — with SYNAPSE_DATABASE_URL (Postgres + pgvector) and an OpenAI-compatible embeddings endpoint (SYNAPSE_EMBED_BASE_URL), the server indexes session summaries, contract resolutions, and repo events as vectors, and synapse_why answers hybrid: the deterministic lexical floor always stands, vector recall only adds semantically-related memories on top (rag: true, numbered citations preserved). pgvector extension/table initialization uses the same advisory-lock discipline as the state store and degrades cleanly if setup fails. Without embeddings, /recall reports degraded: true and the floor answers alone. Only prose is embedded — titles, summaries, rationales, and distilled PR/issue comment excerpts (code blocks stripped, capped at 500 chars; raw bodies are never persisted) — never raw code. SYNAPSE_RAG=0 disables.
Privacy — detection is fully deterministic and local: only symbol-level contracts (signatures, never function bodies) leave the daemon. The older CLI contract resolver sends the computing agent's full file plus its dependency-graph neighbors to the configured model so the merge is caller-aware. Mediator adapt prose is narrower: the server sends only deterministic proposal metadata such as symbol id, before/after signatures, summaries, session ids, and affected call-site paths. Opt out of both resolver and mediator adapt prose with SYNAPSE_LLM_RESOLVE=0 (or leave OPENROUTER_API_KEY unset), or point OPENROUTER_BASE_URL at a local/self-hosted OpenAI-compatible endpoint to keep code on your machines.
Install as a package — the CLI ships as a single self-contained npm package (all five workspace packages, the server, and the Python/Go sidecar assets bundled):
npm install -g @kumario/synapse # installs the `synapse` binaryTo build the same tarball from a checkout (release flow):
node scripts/build-package.mjs # stages + packs dist-release/<name>-<version>.tgz
npm run verify:package # installs from the tarball and smoke-tests it
npm run verify:npm-pack # compatibility alias for verify:package
npm publish --access public dist-release/<tarball> # maintainers onlyThe public name/version live in release.config.json; bump the version there before building, and keep it ahead of npm view @kumario/synapse version.
CI — .github/workflows/ci.yml runs build + typecheck + test plus the full hermetic verify matrix:
npm run verify:all # one build, then every verify
node scripts/ci-verify-all.mjs --only why,doctor # a subset while iterating
SYNAPSE_VERIFY_SKIP=hot-path-latency npm run verify:all # explicit skipsnpm run eval:detection measures the seven conflict-engine rules
(same_symbol_active, same_symbol_unpushed, contract_divergent,
dependency_changed, transitive_dependency, stale_base,
same_file_no_overlap) against a corpus of 25 scenarios in
evals/detection-corpus/ — both synthetic team-state scenarios and real
TypeScript source run through extractTypeScriptContracts →
diffTypeScriptContracts → evaluateConflicts. Per-rule precision/recall is
compared against the committed ratchet baseline in
evals/detection-baseline.json; the script exits non-zero on any regression,
on a new rule appearing without a baseline entry, or on a baseline rule
disappearing from the run. Metrics only move via a deliberate
node scripts/eval-detection.mjs --write-baseline run.
Snapshot from the committed baseline (regenerate with --write-baseline after
deliberately growing the corpus):
| Rule | TP | FP | FN | Precision | Recall |
|---|---|---|---|---|---|
contract_divergent |
1 | 0 | 0 | 1.000 | 1.000 |
dependency_changed |
5 | 1 | 0 | 0.833 | 1.000 |
same_file_no_overlap |
1 | 0 | 0 | 1.000 | 1.000 |
same_symbol_active |
1 | 0 | 0 | 1.000 | 1.000 |
same_symbol_unpushed |
6 | 0 | 0 | 1.000 | 1.000 |
stale_base |
1 | 0 | 0 | 1.000 | 1.000 |
transitive_dependency |
1 | 0 | 0 | 1.000 | 1.000 |
The one sub-1.0 metric is a known precision gap, not a corpus artifact: the
dependency_changed rule fires whenever a direct (1-hop) dependency changes,
regardless of whether compareSignatures classifies the change as
compatible. The dependency_changed_but_compatible scenario in
evals/detection-corpus/dependency.json encodes a teammate adding an optional
parameter to a function another module depends on — a backward-compatible
change — and the engine still flags dependency_changed. This is baselined
as the true number rather than hidden; fixing it (e.g. demoting
dependency_changed to info when compareSignatures says compatible) is
a candidate follow-up for packages/conflict-engine/src/index.ts, out of
scope for this benchmark.
Run with npm run <script>. See package.json for the complete list.
| Script | Verifies |
|---|---|
verify:m0 |
Runnable skeleton + realtime stub loop (milestone 0) |
verify:analyzer-ts / verify:analyzer-py |
Per-language contract extraction, signature diffing, and TS import-edge coverage |
verify:python-check |
Full realtime Python loop → contract_divergent + resolution |
verify:analyzer-go / verify:go-check |
Go contract extraction/diff (warm go/parser sidecar); full realtime Go loop → contract_divergent + resolution. Skips only when no Go toolchain is available; fails if Go is installed but the sidecar cannot build |
verify:daemon-ts-report / verify:file-only-ts-check |
Automatic TS report path; symbol-level conflicts from a file path |
verify:dependency-ts-check |
Warns when a file depends on another's unpushed change through TS dependency edges |
verify:tsx-check |
React-shaped repos: default-exported .tsx component props change → symbol delta + dependency_changed for the importing component; .mjs modules join the same graph |
verify:contract-compat / verify:resolution / verify:mediator |
Compatibility classification; merged-contract resolution; mediator two-phase happy path |
verify:hot-path-latency / verify:large-repo-latency / verify:repo-latency |
Pre-edit hot-path latency budgets (p95 ≤ 50ms, max ≤ 150ms) |
verify:whatsup / verify:why / verify:feedback / verify:insights |
Team briefing; memory search; conflict feedback telemetry; local aggregate coordination insights including mediator proposal counts |
verify:session-summary / verify:session-start |
Layer II session summaries and catch-up briefing |
verify:hooks |
Claude Code join + hook pre/hook post as invoked by Claude Code, including check-before-edit then first post-edit delta reporting |
verify:mcp-adapter |
Stdio MCP adapter tools/resources forwarding to the daemon |
verify:connect |
synapse connect wires up every agent (configs + rules), idempotently, and the MCP server advertises hook-equivalent instructions |
verify:auth / verify:tenancy |
Shared-token and project-key auth paths |
verify:up / verify:up-tunnel / verify:doctor |
Multi-machine setup, tunnels, and preflight diagnostics |
verify:persistence |
State survives a server restart (SQLite, per-entity rows) |
verify:persistence-pg |
Same durability proof on Postgres incl. advisory-locked schema init and SIGKILL; runs when SYNAPSE_VERIFY_PG_URL/SYNAPSE_DATABASE_URL is set (CI service), SKIPs offline |
verify:multi-instance |
Two servers on shared Postgres + Redis, daemons split across them; a report on A is readable in GET /state on B and pushed to B's daemon. Needs SYNAPSE_VERIFY_PG_URL + SYNAPSE_VERIFY_REDIS_URL (CI services), SKIPs offline |
verify:file-watcher |
A manual edit (no report call) emits a contract delta via the watcher; non-analyzable files ignored; SYNAPSE_FILE_WATCHER=0 daemon stays inert |
verify:reconnect |
A delta emitted while the server is down still reaches the team after restart |
verify:metrics |
Structured logs and /metrics counters |
verify:protocol-compat |
Handshake version negotiation: legacy accepted, newer downgraded, out-of-range refused with 426 + range headers |
verify:delta-broadcast |
Protocol v2 clients receive state.delta after mutations while legacy clients keep receiving snapshots |
verify:security |
WS flood → rate_limited acks, state bounded; local daemon JSON 413/400 regressions; webhook 429 past budget; auth-mode server refuses unsigned webhooks (403) until a secret is set, then signed-only |
verify:fuzz |
Seeded malformed-source corpus against all three analyzers: the TS extractor never throws; the Python/Go sidecars answer or reject every request and stay healthy |
verify:why-rag |
Hybrid recall: a question with zero lexical overlap finds the memory through vectors (stub embeddings, advisory-locked pgvector init); the lexical floor alone finds nothing; no provider → degraded: true. Needs pgvector (CI image), SKIPs offline |
verify:adaptive-severity |
Feedback-tuned demotion of noisy warnings |
verify:branch-aware-severity |
Cross-branch stale_base/dependency_changed demote to info; merge-blocking rules and same-branch conflicts still warn |
verify:docker |
Builds the server image, boots it, drives one edit→report |
verify:npm-pack |
Compatibility alias for verify:package so npm-pack and release smoke use the same public tarball |
verify:github-webhook / verify:github-briefing / verify:pr-brief |
GitHub push/PR/review/comment webhooks, catch-ups, and local PR handoff briefing |
verify:all |
One build, then every verify (the CI matrix) |
eval:conflicts |
Recorded conflict eval suite (overlap, breaking, compatible, divergent, …) — hard pass/fail gate on 7 fixed scenarios |
eval:detection |
Detection-quality benchmark — per-rule precision/recall over evals/detection-corpus/ against a committed ratchet baseline (see "Detection quality" above) |
Planning is the source of truth — there is no public docs site.
| Doc | Contents |
|---|---|
synapse-context.md |
Product context and rationale |
synapse-build-plan.md |
Milestone build plan |
synapse-technical-spec.md |
Technical specification |
apps/cli/src/index.ts |
CLI commands and daemon |
| Milestone | Scope |
|---|---|
| 0 | Runnable skeleton and realtime stub loop |
| 1 | TS/Python contract extraction, delta diffing, durable live state, severity scoring |
| 2 | Dependency graph, MCP adapter, GitHub webhooks, cross-agent support |
| 3 | Team briefings |
| 4 | Persistent memory (synapse why deterministic seed, plus optional pgvector/RAG when Postgres + embeddings are configured) |
Working conventions — branching, commit format, PR process, and the CI gate — live in AGENTS.md and apply to humans and AI agents alike. The local loop:
npm ci # install from the lockfile (Node 20)
npm run build # build all workspaces
npm run typecheck # type-check all workspaces
npm run lint # eslint (part of the CI required gate)
npm test # run the test suiteBranch off main as type/short-slug (e.g. fix/daemon-lock-race), use Conventional Commits (feat(scope): …), keep PRs small and focused (they squash-merge into main), and make sure the required CI check passes. Never commit secrets or .env files. See CONTRIBUTING.md for the full version and SECURITY.md to report a vulnerability.
MIT © 2026 Prince Kumar
Built by Prince Kumar