Workspaces
July 11, 2026 · View on GitHub
A workspace is the instance container that binds together the pieces a
Bridge needs to work on one thing: the config overlays it subscribes to, the
member repos it clones into its working set, and an opaque session
pointer. Each workspace is one file — workflow/workspaces/<id>.yaml — and the
engine that reads and mutates it is scripts/workspace.py.
A workspace does not replace the overlay mechanism; it sits above it. Config
overlays are still materialized by the existing, test-locked overlay engine
(scripts/overlay.py) — the workspace layer just
delegates to it and records the overlay by name. What the workspace layer adds on
its own is the second membership dimension the overlay engine never had: code
repos, cloned into an ignored, public-fork-safe location and pinned in a lock.
workflow/workspaces/<id>.yaml ← the workspace definition (what you author)
│
▼
scripts/workspace.py ← the workspace engine (this layer)
├── role: code ─▶ git clone → .bridge/workspaces/<id>/<member>/ (ignored)
│ pin {url, ref, resolved_sha, path} in workspaces.lock.yaml
└── role: config ─▶ subprocess → scripts/overlay.py (UNCHANGED, delegated)
materializes tracked copies, owns overlays.lock.yaml
Quickstart
A workspace goes from empty to a bound working set in a handful of commands. The
mutating verbs (create / subscribe / unsubscribe) require a user/* branch
(see Branch gate); the read-only verbs (list / status /
validate) run on any branch. The /workspace skill
wraps every verb below — it is the ergonomic front-end to this raw engine.
# 1. create the container → writes workflow/workspaces/bigcorp.yaml
python3 scripts/workspace.py create bigcorp --title "BigCorp engagement"
# 2. subscribe a code member → clones into .bridge/workspaces/bigcorp/<member>/
# (ignored) and pins {url, ref, resolved_sha, path} in workspaces.lock.yaml
python3 scripts/workspace.py subscribe bigcorp https://github.com/bigcorp/service.git
# 3. status → per-clone drift (clean / ahead / missing / unpinned) vs the pinned
# resolved_sha; "ahead" is any HEAD≠pinned-SHA divergence, "unpinned" = clone
# present but no lock entry
python3 scripts/workspace.py status bigcorp
# 4. subscribe a config overlay → delegated to scripts/overlay.py as a subprocess;
# the workspace records only the overlay NAME (overlay.py owns overlays.lock.yaml)
python3 scripts/workspace.py subscribe bigcorp https://github.com/bigcorp/bridge-config.git --role config
# 5. list → read-only table: ID TITLE #CODE #OVERLAY DIR
python3 scripts/workspace.py list
# 6. unsubscribe the code member → drops its lock entry, refreshes the exclude
# block, deletes the clone last (metadata before rmtree)
python3 scripts/workspace.py unsubscribe bigcorp service
The member slug in step 6 (service) is the last URL path segment with .git
stripped and lowercased — the same slug the clone landed under in step 2.
Use cases
Concrete shapes the binding container is built for. Each is honest about where the model stops.
-
Client / engagement bundle. One workspace per client: the client's repos as
role: codemembers plus the org's shared config overlay as arole: configmember. Re-opening the engagement means the definition already knows the working set — no rediscovery. Caveat: therole: configoverlay is materialized by the repo-wide overlay engine (overlay.py), so its effect is per-repo, not isolated per workspace — a workspace groups a subscription, it does not sandbox config. -
Multi-repo feature work. A change that spans several services: subscribe each repo into one workspace, work across the clones, and
statusreports per-clone drift (clean/ahead/missing/unpinned) against each pinnedresolved_sha—aheadis the label for any HEAD≠pinned-SHA divergence (behind and diverged included),unpinnedis a clone with no lock entry. The workspace is the shared frame; the members stay ordinary git clones. -
Safe evaluation of third-party code.
role: codeclones land under the gitignored.bridge/tree behind a per-workspace.git/info/excludeblock, so even on a public fork a carelessgit add -Acannot stage — let alone publish — the foreign code. Only trust-guarded URL schemes are accepted (§ public-fork safety). -
One machine, many tools. The shared identity registry (
~/.workspaces/workspaces.json, v2, multi-writer) lets any conformant tool on the box see the same project identities; open-bridge is a full co-writer, not only a reader. Caveat: the registry is a best-effort mirror behind an advisory lock — the repo-local definition + lock stay the source of record, and a co-writer that ignores the lock can transiently clobber a row (§ Known limitations); the next local mutation re-converges it. -
Rebuild a working set on a new machine. A workspace definition is one small YAML file, so the committed definition already names every member. But
subscribeis idempotent: with the definition present it finds the existing member entry and returns "already a member — no change" without cloning, so it does not re-materialize the missing clones on its own.statuscorrectly reports each absent member asmissing; to actually restore a clone,unsubscribethe member andsubscribeit again (a futurematerialize/syncverb will make this one step). Note that even a fresh clone lands on the ref's current tip (git clone --branch <ref>), not the lockedresolved_sha. Caveat:role: configmembers re-materialize through the overlay engine, which is per-repo — the overlay-delegation caveat from case 1 applies again.
Two halves: shared identity + repo-local materialization (Option 3)
A workspace has two separable halves, and open-bridge implements both:
- Identity — which project this is: its name, the directories it roots at,
and its
git_remotes[]. A set of projects is a fact about the machine, not about one repo or one tool, so identity is shared and lives in a tool-neutral registry outside any single tool —$WORKSPACES_DIR/workspaces.json(else~/.workspaces/workspaces.json), schemaversion: 2. It is read/written by the standalonescripts/workspace_registry.py. - Materialization — how this instance realises the workspace locally: the
role: codeclones under.bridge/, the.git/info/excludeblock, and the generatedworkspaces.lock.yaml. A clone location and a generated lock are implementation details of one instance, so this half stays repo-local — it is the engine documented in the rest of this page (scripts/workspace.py).
The shared registry is multi-writer by design — other tools (a TUI knowledge
tool, a cmux importer, an editor or agent hook, and this Bridge) all conform
to one documented protocol and write the same file — so the identity writer is
held to strict multi-writer discipline (see
The shared identity registry below).
This follows the reconcile decision in
work/tasks/workspace-unification/deliverables/RECONCILE-ANALYSIS.md (Option 3):
adopt the shared identity registry, keep materialization repo-local.
Standalone-first — zero co-writer required
The workspace engine is standalone. It runs with no co-writer present — no
import of another tool, no shelling out to any co-writer binary, no external path
assumption. Every
verb (create, list, validate, status, subscribe, unsubscribe) works
end-to-end on a bare Bridge that has never heard of any external tool.
This is a hard invariant, not a nicety. The only provider hooks in the whole
design are two inert seams: the schema's x-provider extension bag (which
CORE never reads) and the subscribe name-resolution seam (which prints a
graceful "provider not available" and exits when no provider is installed). Both
are described under The optional external-provider seam
and neither does anything on a standalone Bridge. A future knowledge/workspace
tool may attach through them later; nothing here waits on
it, and nothing here breaks without it.
The model — overlays[] + repos[] + a session pointer
A workspace definition (workflow/workspaces/<id>.yaml, schema:
docs/schemas/workspace.schema.yaml) is a small,
generic YAML file. Its required core is just identity —
schema_version, id, title — and everything else is optional:
| Field | Meaning |
|---|---|
schema_version | Contract version (1). |
id | Workspace slug (^[a-z][a-z0-9-]*$); must equal the filename basename. |
title | Human label. |
description | One-line purpose. |
directory | Working directory the workspace maps to (${var} / leading ~ allowed). Informational only — a workspace does not fold in instance-lifecycle; see the multi-instance note below. |
created_at / updated_at | Engine-stamped ISO-8601 UTC timestamps. |
overlays[] | The config-overlay subscriptions this workspace pulls (an index; materialization is delegated — see below). |
repos[] | The member repos (each role: code or role: config). |
session_ref | An opaque resume pointer — reserved. The engine treats it as a typed, meaningless string; it is never a semantically shared session concept, and this layer neither reads nor writes it. |
x-provider | The generic extension point — a namespaced bag an optional external provider may attach to. CORE never reads it; extra keys never fail validation (forward-compat). |
The three ingredients are orthogonal:
overlays[]— shared config pulled from org-overlay repos. These are the same overlays described in Org Overlays; the workspace is just a place to group a subscription set.repos[]— member repos. Arole: codemember is source you want in the working set (cloned locally, ignored by git). Arole: configmember is an overlay expressed as a repo URL, which is normalized and handed to the overlay engine.session_ref— an opaque pointer, reserved for a future resume story. It is deliberately not interpreted here, because "session" means different things in different tools and a shared field must not imply a shared concept.
Not an instance re-architecture.
directoryis informational and the workspace never owns instance provisioning, installers, or the multi-instance lifecycle. Several Bridges side by side is still the domain of multi-instance; a workspace is a binding inside one instance, not a new instance model.
Config overlays delegate to the overlay engine
The workspace layer is never a second materializer for config. When you add a
role: config member, workspace.py delegates to overlay.py as a
subprocess — it does not import the overlay engine and does not reimplement any
of its logic:
subscribe <ws> <git-url> --role config
│
▼
subprocess: scripts/overlay.py --repo-root <root> add <git-url> --ref <ref> …
│ (overlay.py's own branch gate, leak gate, conflict/precedence
│ handling, per-file [y] prompts, and exit codes run UNCHANGED)
▼
overlay.py writes the tracked copies + owns overlays.lock.yaml
│
▼
workspace.py records only the overlay NAME in the definition overlays[]
Subprocess (not import) is a deliberate boundary. overlay.py is mature and
test-locked (scripts/tests/test-overlay.sh),
so the workspace layer must sit above it and leave it byte-for-byte untouched.
Shelling out preserves the overlay engine's exact gates and exit codes, keeps the
workspace engine standalone (no packaging), and means all the guarantees in
Org Overlays — merge-never-clobber, precedence, CORE-refusal,
the scope-leak model, prompted-paths-only — apply verbatim to config members.
Consequently the two locks stay cleanly separated:
- Config state lives in
overlays.lock.yaml, owned byoverlay.py. - The workspace lock references those overlays by name only — a back-reference, never a re-pin. There is no duplicated config state.
Unsubscribing a role: config member likewise delegates (overlay.py remove <name>) and then drops the name from the workspace's overlays[].
Repo membership + public-fork safety
role: code members are the new capability, and cloning arbitrary source into a
working tree is a security surface — a fork of a public repo is itself public,
so a careless git add -A could publish foreign code. The engine treats this the
same disciplined way overlay.py treats its cache:
-
Clone into an ignored location. Code members clone to
.bridge/workspaces/<id>/<member>/, under the already-gitignored.bridge/tree (mirroring.bridge/overlays/<name>/). The member slug defaults to the last URL path segment,.gitstripped and lowercased. -
Belt-and-suspenders
.git/info/excludeblock. On everysubscribe/unsubscribe, the engine rewrites a per-workspace marked block in the untracked local exclude file:# >>> workspace:<id> (managed by scripts/workspace.py — do not edit) >>> /.bridge/workspaces/<id>/<member>/ # <<< workspace:<id> <<<It is idempotent, rebuilt on each mutation, and dropped entirely when the workspace has no code members. Because
.git/info/excludeis local and untracked, neither the code nor the member filenames can be published — even on a public fork. The tracked.gitignoreis never touched by this block. -
Git-URL trust guard.
subscribeaccepts onlyhttps://,ssh://, scp-formuser@host:path, andfile://(the last for local fixtures/tests). Every other scheme (http://,git://,ext::,fd::, …) and any argument starting with-(argv-injection guard) is refused before any clone — it writes nothing. -
Deterministic pin. Each code member is recorded in the lock as
{name, url, ref, resolved_sha, path}, withresolved_shathe clone'sHEAD— sostatuscan detect drift andunsubscribecan prune deterministically.
subscribe is idempotent: re-adding a member with the same url + ref is a
no-op (no re-clone, no lock churn); a different url/ref at the same slug is
refused.
The lock model
workspaces.lock.yaml is a generated root file (scope: user), parallel to
overlays.lock.yaml and validated by
docs/schemas/workspaces-lock.schema.yaml.
It records the resolved reality of role: code member clones so the engine
can tell intent (the definition) from what is actually on disk:
schema_version: 1
workspaces:
<workspace-id>:
updated_at: <iso-8601-utc>
repos: # role=code members only
- name: <slug>
url: <uri>
ref: <string>
resolved_sha: <40-hex> # immutable pin, HEAD at clone/update
path: <string> # clone path, relative to repo root
overlays: [<name>, ...] # back-reference to overlays.lock.yaml entries;
# NOT re-pinned here — overlay.py owns that state
Like its sibling, the lock is scope: user: gitignored in a public fork
(it names local material paths) and un-ignored only in a private instance, exactly
as overlays.lock.yaml is handled. The .bridge/workspaces/ clones are always
ignored (covered by /.bridge/). The workspace definitions themselves are
user instances too — tracked in a private instance, absent on a fresh public clone
which ships only _template.yaml (the same policy as workflow/contexts/*.yaml).
State-file locations
| Artifact | Location | Tier |
|---|---|---|
| Workspace definition | workflow/workspaces/<id>.yaml | scope:user instance |
| Definition template | workflow/workspaces/_template.yaml | core (_-prefixed, excluded from discovery) |
| Definition schema | docs/schemas/workspace.schema.yaml | core |
| Generated lock | workspaces.lock.yaml (repo root) | scope:user, generated |
| Lock schema | docs/schemas/workspaces-lock.schema.yaml | core |
role: code member clones | .bridge/workspaces/<id>/<member>/ | ignored |
The definition lives under the workflow/ cluster-wrapper because a workspace
binds what happens when — repos + overlays for a unit of work. It gets its own
<types> folder per the Default-to-Folder rule (see
structure) and is found by the standard discovery glob
workflow/workspaces/*.yaml (skipping _-prefixed files).
The workspace command surface
Increment status. The ergonomic
/workspaceslash-command skill has landed (skills/workspace/SKILL.md,scope: core) — the way thebridge-overlayskill wrapsoverlay.py. It is the front-end; the engine + model (scripts/workspace.pyand the two schemas) stays the contract, and you can always drive it directly. Still open: thebridge-overlay→/workspacerename, an/overlayalias on/workspace(the skill only cross-references/overlay, it does not claim it as a trigger), and theecosystem.yaml workspaces:cleanup — a third, older sense of "workspace" ("which repos belong together") still lives atecosystem.example.yaml, unreconciled with the newworkflow/workspaces/container.
The engine CLI:
workspace [--repo-root R] <subcommand> …
create <name> [--dir DIR] [--title T] [--description D]
list
validate [name]
status [name]
subscribe <name> <git-url> [--ref R] [--role code|config] # alias: add-repo (--role default: code)
unsubscribe <name> <member> # alias: remove-repo
subscribe / unsubscribe are the canonical verb names; add-repo /
remove-repo are retained aliases that dispatch to the identical handlers
(same behaviour, same branch gate) so older scripts and vocabulary keep working.
create— scaffold a newworkflow/workspaces/<name>.yamlfrom the template (schema_version,id,title, timestamps, emptyoverlays[]/repos[]). Refuses an invalid slug or an existing file.list— read-only table of workspaces and their code/overlay counts.validate [name]— validate one or all definitions against the schema (preferringcheck-jsonschemawhen available, with an in-engine fallback otherwise). Anx-providerbag never fails validation.status [name]— read-only drift report: for eachrole: codemember, whether the clone is present and whether itsHEADmatches the lock'sresolved_sha(clean/ahead/missing); config overlays are listed by name.subscribe(aliasadd-repo) — clone arole: codemember (§ public-fork safety) or delegate arole: configmember tooverlay.py(§ config delegation).unsubscribe(aliasremove-repo) — remove a code member (delete clone, drop the lock entry, refresh the exclude block) or a config overlay (delegateoverlay.py remove).
Branch gate
The mutating verbs — create, subscribe, unsubscribe (and their aliases
add-repo / remove-repo) — refuse to run unless the current branch is user/*
(or detached / non-repo). This is the same
gate overlay.py enforces and it honours the same env escape
(BRIDGE_OVERLAY_ALLOW_ANY_BRANCH=1) so a shared test harness works. The
read-only verbs (list, validate, status) run on any branch.
Exit codes
| Code | Meaning |
|---|---|
0 | success |
1 | WorkspaceError — branch gate, validation failure, trust-guard refusal, not-found, or a delegated overlay.py non-zero |
2 | usage / repo-root / chdir error (argparse default) |
3 | the optional provider-name seam fired but no provider is available (see below) |
The optional external-provider seam
Two inert extension points let a future external provider attach to the workspace model without CORE ever depending on it. Both are non-normative and do nothing on a standalone Bridge.
-
x-provider(schema). A namespaced bag on the workspace definition (propertyNamesare provider slugs). An external provider may stash its own data there; CORE never reads it, and unknown keys never fail validation. No co-writer-specific field is ever a required CORE field — the required core stays generic. -
The
subscribename seam. The second positional ofsubscribe(aliasadd-repo) is normally a git URL. If instead it is a bare slug (^[a-z][a-z0-9-]*$, no scheme, no/), the engine treats it as a workspace/provider name that only an external provider could resolve to a set of repos. On a standalone Bridge no provider is present, so the engine prints a graceful message and exits 3 — performing no import and no path lookup of any provider:'<x>' is not a git URL. Resolving a workspace/provider name to repos needs an external provider that is not available in this standalone Bridge. Pass an explicit git URL, or install a provider.
The intent, non-normatively: a knowledge/workspace tool that maintains its own workspace registry (a TUI knowledge tool being the concrete example) could, in a later increment, register as that provider and resolve a workspace name to the repos it knows about — because both sides read the same open, versioned workspace schema. That is a documented seam, capability-detected and graceful when absent — never a build dependency, and explicitly not built here.
The shared identity registry (~/.workspaces/)
The identity half (above) is a conformant reader/writer of the tool-neutral
registry, implemented standalone in
scripts/workspace_registry.py — pure stdlib,
with no external tool imported, shelled out to, or assumed present. It is a
first-class writer, not merely a reader: a standalone Bridge registers a
workspace's identity itself, without waiting on any other tool.
Location + resolution
$WORKSPACES_DIR if set, else ~/.workspaces/ — one predictable, cross-OS path,
deliberately no XDG special-casing. The directory is created on first write. The
registry format's normative owner remains the external design that specifies
workspaces.json v2 (this writer conforms to it and does not redefine it). This
repo additionally ships a descriptive companion —
docs/schemas/workspaces-registry.schema.yaml
— a machine-readable JSON Schema of the field set the known writers actually
emit, kept here for local check-jsonschema validation and IDE support. It
documents the format; it does not own it.
Multi-writer protocol (safety-critical, but advisory)
Because other tools write the same workspaces.json, a bug that drops a field
corrupts their registry, not just ours. Every mutation this writer makes follows
one protocol:
take the advisory lock (
<dir>/.lock,flock) → read → modify in memory → atomic replace (workspaces.json.tmp+os.replace) → release the lock.
The lock is advisory, not mandatory — it protects cooperating writers only.
flock coordinates writers that take it; it cannot stop a co-writer that
bypasses the lock, or that holds a long-lived in-memory copy of the registry (a
TUI that reads once at startup and later blind-saves that stale state, for
example), from clobbering a mutation this writer just made. When that happens
the shared mirror is transiently wrong — but it is a best-effort mirror, not
the source of record: the repo-local definition/lock stay authoritative, and
the next local create / subscribe / unsubscribe re-publishes and
re-converges the row. See Known limitations below for the
concrete co-writer gap this covers.
Invariants this writer holds itself to on every mutation — asserted by
scripts/tests/test-workspace-registry.sh,
whose mutation-checks break the engine to prove each assert has teeth:
- Preserve unknown fields — unrecognized top-level keys and unrecognized per-workspace keys round-trip untouched.
- Never touch another tool's slice — a writer edits only the shared identity
fields plus its own
extensions["open-bridge"]; every otherextensions["<tool>"]slice is left byte-for-byte. versionis max-monotonic, and coerced. An on-diskversionof2,"2", or2.0all read as2; a coerced version higher than2may be read but is refused for write (a clean non-zero error, never a clobber). Writes always emitversion: 2(int).- De-dup on create by identity, on BOTH create paths.
upsert_workspacede-dups on a shared normalizedgit_remotesentry or a shared canonical directory path (§ Path identity below).publish_workspaceadditionally performs a guarded structural adopt: with no id match, exactly one structural candidate — excluding any row that already carries anextensions["open-bridge"]slice (that is another instance's or another writer's own mirror, never adopted into) — is adopted with MERGE semantics. This is the protocol's de-dup rule, not a convenience shortcut; zero or ≥2 candidates still mint a fresh row (see Known limitations). - Fail closed on anomalies — never a silent reset. An unparseable file or a
missing/non-numeric
versionrefuses the write (RegistryError); the on-disk bytes are left exactly as found, nothing is rotated or guessed. Only a genuine older file (coercedversion <= 1, the documented v1→v2 cutover) is rotated — to a timestampedworkspaces.json.bak.<UTC yyyymmddThhmmssZ>(never a single reused slot, so a second rotation can never destroy an earlier evacuation), with a loud stderr notice (backup path + evacuated row count) — before a freshversion: 2registry is started. This is deliberately stricter than the upstream design doc's default of rotating on any unreadable file: silently discarding a co-writer's live rows on a survivable parse hiccup is worse than refusing the write and asking a human to look, so that is what this writer does.
Reads (read_registry, list_workspaces, find_by_path) are lockless — the
atomic replace guarantees a reader always sees a whole file, never a torn one.
Known limitations
- Convergence is attempted, not guaranteed. A publish converges onto a peer
tool's pre-existing row only when there is a directory or git-remote match
(the guarded structural adopt above) and that candidate is unambiguous.
Two cases still mint a second row instead of converging: (a) ≥2 structural
candidates — the adopt is deliberately conservative and refuses to guess
which one is "the" project; (b) the only structural match already carries a
foreign
extensions["open-bridge"]slice — treated as another instance's or another writer's own mirror, never adopted into. Both need a cross-tool reconciler (not yet built) to merge by hand or by policy. - Convergence needs something to match on. A workspace with neither a
directory:nor anyrole: codemember publishes nodirectories[]and nogit_remotes[]— there is nothing for a structural match to key on, so it can only ever mint its own row (or find itself by id on a later publish); it can neither adopt nor be adopted into a peer tool's row. - The lock is advisory (see above) — a co-writer that saves from a stale in-memory copy while holding no lock can still clobber a mirror row between two of our publishes. The next local mutation re-publishes and repairs it, but the window is real, not theoretical — a long-running TUI is the concrete case today.
Path identity + matching
Paths are canonicalized (symlinks resolved, ~ expanded) and
alias-expanded for the macOS File-Provider spellings
(~/Dropbox ↔ ~/Library/CloudStorage/Dropbox; the same shape generalizes to
OneDrive / Google Drive). find_by_path matches a query equal-to or nested-under
a workspace directory, and the longest matching directory wins (the most
specific workspace). Both spellings are stored in directories[].aliases so cheap
string-prefix consumers match without canonicalizing.
Writer API
| Call | What |
|---|---|
upsert_workspace(name, directories=[…], git_remotes=[…], open_bridge_ext={…}) | Create-or-update by structural identity (shared path / git remote), MERGING — the generic cross-tool converge path. |
publish_workspace(ref, name, directories=[…], git_remotes=[…], open_bridge_ext={…}) | The owning mirror: (1) an id match on a prior publish (ref, parked in extensions["open-bridge"]["id"]) REPLACES the mirrored identity so a removal shrinks it; (2) else exactly one structural (path/remote) candidate with no foreign open-bridge slice is ADOPTED with MERGE semantics (§ guarded structural adopt above); (3) else a fresh row is minted. Used by the engine write-through. |
read_registry() / list_workspaces() | Read the whole registry / the workspace rows. |
find_by_path(p) | Longest-match workspace for a path, else None. |
archive_workspace(id) | Soft-delete (archived: true). |
Automatic write-through from the repo-local engine
The repo-local engine (scripts/workspace.py) publishes
identity automatically: after a create / subscribe / unsubscribe succeeds, it
mirrors the workspace into the shared registry via publish_workspace — title → name,
the workspace's own directory: (interpolated + canonicalized) as the PRIMARY, unlabelled
directories[0] entry when set, each role: code member's clone directory after it (label
repo) + its origin remote, and the overlays/repos under extensions["open-bridge"]. The
mirror is keyed by an instance-qualified id — sha256(realpath(repo root))[:12]:<slug>
— so two Bridge instances that happen to share a workspace slug (see
Multi-instance) never clobber each other's row. Successive publishes from
the same instance converge on one
entry by that id, and an unsubscribe shrinks the mirror. This is additive: the
repo-local definition, lock, and materialization stay the source of record, and a
shared-registry hiccup (a version-guarded newer file, a fail-closed anomaly) warns but never
fails the local command — local materialization is already done. The publish target
resolves the usual way ($WORKSPACES_DIR else ~/.workspaces/), so an instance's tests must
pin $WORKSPACES_DIR to a temp dir (both suites do). A workspace with no directory: and no
role: code members publishes an empty directories[] — see
Known limitations.
How our model maps onto the shared schema
open-bridge keeps its own vocabulary internally and maps it at the registry boundary — it does not rename load-bearing fields:
| open-bridge (repo-local) | shared registry (v2) | mapping |
|---|---|---|
title | name | mapped on write (the shared schema keeps a title read-alias) |
schema_version | version | same envelope pattern |
directory | directories[0] (unlabelled, PRIMARY) | published when set; omitted (no fallback) when unset |
code members (repos[] role: code) | directories[] (label repo) + git_remotes[] | shared identity |
config overlays (overlays[]) + repo config | extensions["open-bridge"] | our namespaced slice |
materialization (.bridge/ clones, workspaces.lock.yaml, exclude block) | — | stays repo-local; the registry only references identity |
open-bridge defaults (documented, adjustable)
These are open-bridge's chosen defaults for the shared model — written down so they are explicit and easy to revisit, not hard invariants:
- Config overlays + member-repo config live under
extensions["open-bridge"], not as shared fields. The shared core is kept deliberately small: code members map onto the shareddirectories[]/git_remotes[], while config-overlay subscriptions are open-bridge-specific and stay in our extension slice (they may later be proposed as shared fields). titlestays the internal name and is mapped tonameon write. open-bridge does not rename its internaltitle(load-bearing in the lock + template).- This Bridge is a full conformant WRITER, not only a reader — standalone operation requires registering identity with no other tool present.
- The registry is machine-global; materialization is repo/branch-scoped.
Overlay materialization applies only to a workspace whose
directories[0]is a Bridge repo; for a non-Bridge workspace theextensions["open-bridge"]slice is simply empty.
Engine with no direct skill surface. This is the registry engine — the reader/writer and its protocol. No skill exposes its raw verbs directly; instead it is written-through automatically by the repo-local engine's mutating verbs (
create/subscribe/unsubscribe) — which fire equally on a directpython3 scripts/workspace.pyrun and via the/workspaceskill that fronts them (see Automatic write-through above). Registry-only operations that have no repo-local trigger (e.g.archive_workspace, a directfind_by_path) still run viascripts/workspace_registry.pyor the library API.
Schema reference
docs/schemas/workspace.schema.yaml— the workspace definition (JSON Schema Draft 2020-12 in YAML). Field-by-field constraints, theOverlayRef/RepoMember$defs, and thex-providerextension point.docs/schemas/workspaces-lock.schema.yaml— the generated lock (scope: user) recording resolvedrole: codemember clones and the by-name back-reference tooverlays.lock.yaml.docs/schemas/workspaces-registry.schema.yaml— a descriptive (not normative) JSON Schema for the shared~/.workspaces/workspaces.jsonregistry file, § The shared identity registry above.
The definition template ships at workflow/workspaces/_template.yaml; read it and
the matching schema before authoring a workspace by hand (per
file-creation).
Verification
The end-to-end runbook that exercises every verb, both guards (branch gate + trust
guard), the public-fork exclude block, and the shared-registry write-through on a
throwaway Bridge is
docs/workspace-acceptance-test.md — a
zero-prior-knowledge walk that proves the guarantees on this page against the live
engine.
Related
- Org Overlays — the config-overlay mechanism a workspace
delegates to for every
role: configmember, and the source of the merge/precedence/leak guarantees that apply to those members. - Multi-instance — several Bridges side by side; a workspace is a binding inside one instance and does not own the instance lifecycle.
- Structure — the Default-to-Folder layout that places workspace
definitions under
workflow/workspaces/. scripts/overlay.py·scripts/workspace.py— the overlay engine (delegated to, unchanged) and the workspace engine (this layer).