Workspaces

Knowl workspaces provide linked federation across related repositories without merging their databases.

# Create a machine-local workspace.
knowl workspace init product

# Run inside each repository that should join it.
knowl workspace add product
knowl workspace status

# Bare: pick from a list, with the categories worth sharing already ticked.
knowl workspace promote

# Or name them, which skips the picker and dry-runs until --apply.
knowl workspace promote --category decision
knowl workspace promote --category decision --apply

A repository joining a linked workspace records defaultVisibility: workspace, so the knowledge it writes from then on is readable by its peers. The command prints that a default decided it and how to decline; --default-visibility repo opts out, and knowl workspace set with no flags prints the current value. Repositories already listed in a manifest are never moved: an absent defaultVisibility still resolves to repo, because changing what omission means would publish every linked repository's next write on account of an upgrade rather than a decision, and there is no demote. Knowledge written before the default, or under --default-visibility repo, stays private until it is promoted.

For another checkout or machine, copy the workspace manifest and join from each repository:

knowl workspace join /path/to/workspace.json --name api

The shipped workspace commands are:

Command Purpose
knowl workspace init <name> Create a workspace outside its member repositories
knowl workspace add <name> [--name <repo-name>] [--default-visibility <repo|workspace>] [--promote-existing] [--force] Link the current repository; shares its new writes by default in a linked workspace
knowl workspace join <manifest> [--name <repo-name>] [--force] Adopt a copied manifest and map this checkout
knowl workspace list List workspaces known to this machine
knowl workspace status [--verbose] Show this repository's membership and peer health
knowl workspace remove <repo-name> [--export-first] Unlink the current repository, retiring its name if it still owns atoms
knowl workspace promote [--category <list> | --id <id...>] [--apply] Share locally owned atoms with linked repos. Bare opens a picker with decision, constraint, architecture, goal, skill preticked and confirming applies; --apply is only needed on the flag path
knowl workspace repin-embedding [--yes] Move the workspace to this repository's embedding model and list the peers that must reindex

Federation and ownership

The external manifest contains machine-local checkout paths. Membership is two-sided: the manifest names the repository, and that repository's configuration points back to the workspace. Every member continues to own a separate .knowl/knowl.db.

Normal workspace add refuses to link when .knowl/config.json is tracked by Git. --force bypasses only that tracked-config guard; it does not repair embedding-identity mismatches or item ownership.

Only an explicit current query fans out to available peers. A promoted peer result is labeled with its repo and is read-only from the querying repository — unless the call names that repo, which runs it as that repo rather than reaching across from this one; see Doing a linked repo's work from here. A shared peer atom can also be opened whole by id, without acting as anything: see Reading a linked repo's atom by id. Mutations, historical asOf queries, recent context, context packs, work loops, synthesis, code indexing, and implicit lifecycle context remain local. Missing, unreadable, or schema-incompatible peers are skipped and disclosed in the response rather than causing healthy local retrieval to fail.

One ranker, pointed at each repo. Retrieval takes an explicit database handle, so a linked repository is searched by the same code that searches the local one: the same FTS and vector selection, and the same recency, confidence, freshness, category and exact-identifier scoring. Candidates are selected per repository, then scored in a single pass over all of them together — recency is normalized against the candidate set it is given, so ranking each repository separately and combining the results would make every repository's newest atom equally recent. Identical content held by two repositories is deduplicated before the result cap, keeping the local copy, so a shared fact cannot consume two slots and return a short list. A peer handle is opened query_only, and the visibility predicate is applied inside the SQL, so a peer's repo-private row is never read into the querying process at all.

Cross-repository overlap is reported on write. A knowledge write inside a workspace also checks the linked repositories, and reports an exclusive conflict key or a same-subject atom held elsewhere, naming the owning repository. Both the single-atom and batch writers do this, the batch per atom. It is advisory and never mutates: that atom belongs to another repository, knowl_update refuses foreign ids, and only its owner can retire it. Bounded to a few candidates per peer, and non-fatal — an unreadable peer yields no report rather than a failed write. Outside a workspace it costs one check.

Promotion is preview-first and accepts active, private, locally owned atoms selected by category or ID. Both workspace add and workspace join enforce a compatible embedding identity, reject a nested checkout, and refuse a Git-tracked .knowl/config.json without --force. Nothing reaches across from one repository into another: a foreign id is refused, and the only way to change a linked repository's knowledge is to run the call as that repository, which makes the atom local and the guard satisfied rather than bypassed. There is no demote/unshare command, and no workspace-wide historical view.

workspace remove --export-first is an acknowledgement that the repository still owns knowledge; it does not create an export. A removed name is retired only when the repository still owns active atoms, since the name is the ownership key on everything it wrote. A repository that owned nothing releases its name for anyone; a repository that owned atoms keeps exclusive claim on the name and reclaims it by re-linking, while every other repository is refused.

Reading a linked repo's atom by id

A federated query returns rows from every linked repository, each labeled with its owner. Asking for one of those rows wholeknowl_query { id } — resolves it too, so reading a federated result no longer means switching repositories.

The record crosses; the checkout-relative fields do not. A foreign atom arrives with its content, reasoning and alternatives, and without affectedPaths or evidence, because those name files in the owning repository's checkout and resolve against its database and working tree — answered here they would be measured against the wrong tree. It carries a foreign block naming the owner, so an absent affectedPaths reads as deliberate rather than as an atom that cites nothing, and so the agent knows where the item can be changed.

It reaches exactly the rows a query reaches, and no others. A repository's private knowledge stays private until workspace promote shares it; knowing an id is not a way around that, since ids are not secret — they travel in supersession chains and conflict reports. A private row is reported as not found rather than as a refusal, because "that one is private" would confirm it exists.

Reading is all this grants. Updating, superseding or retiring another repository's item is refused by the same guard as before, with the same message. A miss says the linked repositories were searched too, and says "readable from here": a repository that is not checked out on this machine was never asked, and must not be reported as one that answered no.

Ownership

origin_repo is stamped when an atom is created, so every write made while linked is owned and promotable. Joining additionally backfills the rows that already existed, which are by definition the joining repository's own.

written_by records the repository whose session authored an atom, when that is not the one that owns it — the case that arises from naming a repo on a call. Ownership is unaffected: the atom is the target's, and the target promotes and retires it. NULL means the owner wrote it, which is the ordinary case, so the column is set only when author and owner genuinely differ; stamping a repository's own name on its own atoms would make the field say nothing on almost every row. Rows predating the column are NULL for the same reason rather than as a gap — they were written before a repository could act as another, so their owner is exactly who wrote them. It is not part of the lifecycle fingerprint: authorship is fixed at write time and never diverges, so including it would make every cross-repository atom look changed to a peer holding the same one.