Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-010: Workspace federation: leve, opt-in, registry-based

Date: 2026-05-24 Status: Accepted Author: Rômulo Febasi


Context

The product premise (README, line 1) is “for the developer who, alone and AI-augmented, juggles more projects than a team used to”. v0.1 ships a perfect single-workspace experience but is silent about the plural: when the same user maintains three or four workspaces (per client, per domain, per audience), there is no way to:

  • discover them (“which workspaces exist on this machine?”),
  • query across them (“who owns the payments API: in any of my workspaces?”),
  • expose them as one surface to the AI assistant via MCP.

syn today behaves like git: command runs in the workspace it can find walking up from cwd. Outside any workspace, every command but init/setup errors out with Error::NoWorkspace. That is correct for the workspace-local use case and wrong for the multi-workspace one.

The user’s proposal was: have syn init register the workspace in a global SQLite owned by syn, expose CLI + MCP queries over all of them, and add syn remote add <git-url> for remote integration.

This ADR decomposes that proposal critically and locks in the chosen shape before issues are opened.

Decision

Federation lands in v0.2 as seven layered tickets (SYN-75 … SYN-79 plus SYN-90 … SYN-92 for the identity card). The architecture is leve por design: the global database stores metadata pointers to workspaces, never the content of their entities; the identity of each workspace lives in a versioned SYNAPSE.md in its own root.

F1: Workspace registry (SYN-75)

A SQLite file at the etcetera-resolved global path (~/Library/Application Support/synapse/workspaces.db on macOS, $XDG_DATA_HOME/synapse/workspaces.db on Linux) tracks every workspace the user has ever syn init-ed on this machine.

Schema (single table; small, append-friendly):

CREATE TABLE workspaces (
    id            TEXT PRIMARY KEY,        -- short slug derived from root path
    name          TEXT NOT NULL,           -- display name (defaults to dir basename)
    root          TEXT NOT NULL UNIQUE,    -- absolute, canonicalised path
    git_remote    TEXT,                    -- auto-detected from .git/config on register
    federated     INTEGER NOT NULL DEFAULT 0,  -- privacy gate (SYN-79)
    last_seen     TEXT NOT NULL,           -- ISO-8601, refreshed on every syn command
    registered_at TEXT NOT NULL,
    notes         TEXT
) STRICT;

syn init writes the row (idempotent on root). Every command that runs successfully refreshes last_seen. git_remote is auto-detected from .git/config on registration (the workspace root is often a git repo; if not, the column stays NULL).

F2: Federated read commands (SYN-77)

--all flag on read commands (search, info, graph) iterates the registry, opens each workspace in turn, runs the existing core::* use case, and merges results. Output always carries the workspace identifier so the consumer (human or AI) knows where each hit came from.

$ syn search "api pagamentos" --all
client-alfa/      api/pagamentos       API Pagamentos
client-beta/      note/decisao-pgto    Decisão de Pagamentos
personal/         (no hits)

Federated queries are read-only. Writes always target the local workspace (the one Workspace::find resolves from cwd).

F3: MCP federation (SYN-78)

The MCP server exposes three new tools alongside the existing four:

  • list_workspaces: registry contents.
  • federated_search: search_knowledge across opt-in workspaces.
  • get_entity_from(workspace, type, id): read a specific entity from a non-current workspace.

The existing search_knowledge, get_entity, list_by_type, project_graph continue to act on the current workspace only. Adding federated_* cousins instead of overloading existing tools keeps the v0.1 contract intact (BR-11.1).

F4: Git remote awareness (folded into SYN-75)

Captured automatically on registration. No syn remote add in v0.2, that command is reserved for F6 (cross-machine federation), which stays in the backlog (SYN-210).

F5/F6: Cross-machine sync and remote federation

Kept in the backlog (SYN-209, SYN-210). They require design decisions this ADR explicitly defers:

  • Sync model (CRDT? git-driven push/pull? merge conflict handling?)
  • Authentication for cloning remote workspaces
  • Cache invalidation strategy
  • Performance bounds for N-workspace federation across N-machine sync

Privacy gate (SYN-79)

Federation is opt-in per workspace. Default federated = 0 on register. Activation is explicit:

syn ws include <name>   # set federated = 1
syn ws exclude <name>   # set federated = 0

This addresses the “client A workspace cannot leak into client B queries” concern. --all skips workspaces where federated = 0.

F7: Workspace identity card SYNAPSE.md (SYN-90/91/92)

The registry knows that a workspace exists. It does not know what it is about. Without that, list_workspaces over MCP returns paths and slugs: useless for routing a question.

Each workspace ships a single identity file at its root:

<workspace>/
├── SYNAPSE.md            ← identity card (NEW)
├── .synapse/             ← tool area (existing)
├── projects/             ← entities (existing)
├── people/
└── …

Why root, not .synapse/

  • .synapse/ is the “tool area”: disposable index, config, caches. The manifest is content: versioned, diffed, shared, owned by the human.
  • A root-level SYNAPSE.md mirrors the README.md convention, obvious, discoverable, hand-editable.
  • .gitignore rules that hide .synapse/ (recommended) do not accidentally hide the manifest.

Shape

YAML frontmatter for queryable fields, free Markdown body for the human/AI brief. Same parser, same atomicity, same i18n discipline as every other entity in the codebase:

---
# identity
name: Client Alfa Workspace
description: |
  Knowledge base for Cliente Alfa: payments integration, vendor
  contracts, team contacts. Focus on B2B payments and webhook reliability.

# categorisation (consumed by MCP list_workspaces routing)
topics: [payments, webhooks, b2b, vendor-management]
tags: [client-alfa, pagamentos]

# stakeholders (referenced as person slugs IF they exist locally)
maintainer: maria-silva
language: pt-BR

# advisory privacy hints: the real privacy gate is registry.federated
visibility: private        # private | shared | public
shareable: false

# meta
last_curated: 2026-05-24
---

## Quick brief

This workspace covers the Client Alfa engagement: the payments
integration, the focal contacts, and active migrations. Use this
workspace when asked about Alfa, Acme Corp (the vendor), or webhook
v2 work.

## Out of scope

- Personal projects (those live in `~/personal-brain`)
- Cliente Beta work (separate workspace, `~/work/client-beta`)

Cache, not source of truth

SYNAPSE.md is the source of truth: same rule as BR-1.4 for entities. The registry caches name, description, topics, tags, language on:

  • syn init (initial create + write the file)
  • syn ws refresh [<name>] (manual; or auto when the file mtime beats the cached last_seen)
  • the watcher (SYN-65), when it lands

workspaces.db gains columns: description TEXT, topics TEXT (JSON array), tags TEXT (JSON array), language TEXT, manifest_updated TEXT. Everything optional, NULL on legacy rows from before this ticket.

Read and write surfaces

CLI (SYN-91):

  • syn ws edit [<name>]: open SYNAPSE.md in $EDITOR. On save, re-cache.
  • syn ws refresh [<name>]: re-read SYNAPSE.md, update cache. No args = refresh all known workspaces.
  • syn ws topic add|rm <topic>: append-only mutation of the topics list, atomic via Workspace::tx.
  • syn ws tag add|rm <tag>: same for tags.

MCP (SYN-92):

  • get_workspace_manifest(workspace): returns the full document (frontmatter + body) for an opt-in workspace.
  • add_workspace_topic(workspace, topic): proposes an append. Lands in the pending queue (BR-12.3, SYN-60). Same for tag.
  • list_workspaces (cousin of SYN-78) now returns {id, name, description, topics, tags, language} per row: the routing signal the AI needs.

Why “AI can curate the card”

The AI assistant (interacting with the user across sessions) is in the unique position to notice what a workspace is actually becoming about. If, over weeks of Claude Code sessions, an audit topic emerges in 80% of the conversations rooted on a workspace, the AI proposes add_workspace_topic(workspace, "audit"). The user sanctions (BR-12.3) and the next session benefits from the sharper routing. The manifest evolves with use, not with one-shot curation.

Shareable in the future

Since SYNAPSE.md lives in the workspace folder, it travels with the directory. When SYN-210 (remote federation via git) lands, a clone of team/brain-shared.git immediately exposes its manifest, no extra protocol needed. The card is the contract.

Trade-offs

  • + list_workspaces over MCP becomes routing-useful: the AI picks the right workspace before even searching.
  • + Identity is versioned in git alongside content. Diffs of SYNAPSE.md are part of the project history.
  • + Same parsing/validation discipline as the rest: no new format, no new tooling.
  • − One more file per workspace. Mitigated by syn init bootstrapping it automatically.
  • − Cache invalidation. Mitigated by mtime check on every command that touches the registry, plus explicit syn ws refresh.

Consequences

  • + Solves the user-stated problem with a small surface (one table, one flag, three MCP tools, four CLI flags).
  • + Preserves BR-1.4 (“nada vive só no índice”): the global DB has no content of its own, only pointers. Wiping workspaces.db is recoverable by re-running syn init in each root.
  • + Preserves the v0.1 CLI/MCP contract: federation lives behind --all and behind new MCP tools; existing behaviour unchanged.
  • + Privacy by default: opt-in flag keeps confidential workspaces out of cross-queries.
  • − Federated query cost is O(N workspaces). Acceptable for realistic N (≤ ~20); above that needs a persistent index, that’s a separate ticket if/when demand surfaces.
  • − Two databases to reason about (workspaces.db + each workspace’s index.db). Mitigated by giving the registry a single, trivial schema.
  • − Moved-or-deleted workspaces leave stale entries. Handled by syn ws gc (sweep paths that no longer exist; non-destructive, warns before removing).

Alternatives considered

  • Federation pesada (content mirror in global DB). Rejected. Doubles storage, reintroduces sync/consistency between workspace and global, violates BR-1.4, and a deleted workspace orphans its rows. The leve registry has none of these problems and the query-on-the-fly path is fast enough.
  • No registry at all; scan ~ for .synapse/. Rejected. Slow, fragile (skips workspaces outside ~), no place to store federated flag or git_remote auto-detected.
  • Federation always-on (no opt-in). Rejected. Clients with confidential data deserve a default of “do not leak”. --all defaulting to “all you opted in” is a safer default than “every workspace I find”: even on a single-user machine.
  • syn remote add <git-url> in v0.2. Rejected. The user proposed it; it’s a great idea, but it pulls in clone caches, auth, sync semantics. Tracked in SYN-210 (backlog) and revisited when local federation has flown in production for a while.