A repository can compose independent repositories as virtual directories. The parent owns its files and the mount declarations; each child retains its own history, permissions, locks, working copy, and submissions. This first version supports one level of mounts through the same server endpoint, including a gateway.
Declare and select mounts
Submit declarations in the parent's .scribe/config.toml. No second manifest
or lockfile is needed. For example, the fictional orbit-game repository can pin
its shared engine separately from its large art-source repository:
version = 1
[mounts.engine]
path = 'engine'
repository = '01010101010101010101010101010101'
branch = '02020202020202020202020202020202'
revision = '42'
[mounts.art]
path = 'art-source'
repository = '03030303030303030303030303030303'
branch = '04040404040404040404040404040404'
revision = '18'
[profiles.programmer]
include = ['game', 'engine']
[profiles.artist]
include = ['game', 'engine', 'art-source']Replace the example IDs and revisions with actual repository/branch identities and accepted revisions. IDs are 32 lowercase hexadecimal digits. The revision is a canonical unsigned decimal string, preserving all 64 bits through TOML, TypeScript, and JSON. Pins never float with the child branch head. Branch IDs survive renames. The declaration is read at the parent's selected branch/revision, not from unsubmitted local edits or exclusively from main.
Mount names follow profile-name rules. A configuration declares at most 64
mounts; a 65th is rejected even when the file stays within the 64 KiB config
limit. Paths follow portable canonical path rules. Overlapping paths,
case/normalization collisions, config-file overlaps, and self-mounts are rejected.
A mount path cannot name a .scribe directory (in any letter case) or lie inside
the root .scribe/.
The server rejects parent files at, above, or below mount paths, including merges.
When adding a mount over existing parent files, remove those parent files in the
same submission or an earlier one. Accepted history remains unchanged.
Retiring a mount is judged the same way, against the mounts accepted at the
parent change: one change may retire [mounts.NAME] or write files at its path,
never both. Collaborators still have the child workspace on disk at that path
when the change arrives, so the retirement has to be accepted history before
anything may take the path. Whatever the server accepts, a client refuses to
displace a nested workspace and reports the path as failed instead; move the
child workspace out to unblock it.
Profiles select mounts using the existing inclusion prefixes. Selecting engine
selects that child; selecting engine/Source creates a child view for Source.
A broader inclusion or the full repository selects every mount it covers.
Unselected mounts require no target requests and transfer no target content.
scribe workspace create orbit-game ./work --profile programmer
cd work
scribe workspace mounts
scribe syncworkspace mounts lists declarations at the parent branch head and indicates
selection using the current saved view. It does not probe child access. sync
reports each mount as ready, excluded, unavailable, or retained; JSON
includes the name, path, repository ID, branch ID, revision string, and detail.
Add, change, or remove a mount
Mount declarations are ordinary parent configuration edits. To add one, obtain
the child's stable repository and branch IDs and an accepted revision. scribe repo info CHILD shows its repository and main-branch IDs; scribe branch list
inside an existing child workspace shows its other branches. Preserve the
existing config's profiles, lock rules and other declarations while adding the
new [mounts.NAME] table. For an already tracked config:
scribe edit .scribe/config.toml
# Add the mount table in your editor, using actual IDs and a quoted revision.
scribe submit -m "Mount the shared engine"
scribe project syncIf the configuration does not exist yet, create it with version = 1 and the
mount table (run mkdir -p .scribe first), then use
scribe add .scribe/config.toml before submitting. Include the config in your
workspace view before editing it.
To change only a revision, scribe project update NAME --revision N stages the
pin under normal lock rules; review and submit it. To change a path, repository,
or branch, edit the table and submit. To remove a mount, remove its entire table
and submit the config. Existing child directories are retained, as described
under edit, update, and recover.
On an authenticated server, advancing a pin is Writer work; adding, removing,
renaming, or retargeting a mount on main requires a Maintainer, because it
decides where every collaborator's nested workspace lives.
Each shared declaration or pin change becomes effective through a parent commit. Editing a file or staging a pin alone does not publish it. Profiles and local view overrides can select fewer mounts without changing their shared pins or creating a parent commit. Different parent branches and historical revisions can contain different mount sets and pins.
The web interface offers Add mount, edit, and remove actions, subject to the same permissions: a Writer can advance a pin, and changing the topology on main needs a Maintainer. The editor keeps the revision pin editable for Writers on main, disables topology controls with an explanation, and checks permissions again when preparing the change. Writers can edit topology on other branches; accepting those changes onto main requires a Maintainer. Permission refusals preserve entered values and refresh the displayed permissions; uncertain submissions retain their original operation ID for outcome recovery. Those actions submit the same parent configuration changes; they do not edit or delete the child repository. See web mount editing.
Permissions and failure
Reading A's declaration never grants access to B. A reader can see the declared path, IDs, and pin because those bytes belong to A. Each selected child's metadata, registration, and downloads use B's repository identity and the caller's own credential. A child uses its own active lock policy for edits and submissions.
Sync resolves metadata for the entire selected composition before applying files.
If selected B is inaccessible, accessible parent files and other selected mounts
can still sync. The overall result is incomplete and the mount is reported as
unavailable. Once every parent entry is applied, the parent watermark publishes
and its journal closes: an unreadable child never leaves the parent workspace in
a permanent "sync in progress" state that would block moves, merges, or view
changes. The failure stays recorded for the parent, and the next scribe sync
retries the mount. Errors use the same unavailable message for missing and
forbidden targets. Grant access and rerun scribe sync, or cancel/change the
selection to exclude the mount. Unfinished child downloads stay in that child's
own journal and resume there.
Revocation denies future remote operations according to the existing authorization policy. Downloaded files and local edits are preserved. Access cannot retract already downloaded content. Parent access grants no child write permission.
Edit, update, and recover
An initialized child is an independent workspace with its own local state in
engine/.scribe/_state/. Commands
run inside it address the child repository. Parent discovery and recursive add,
including --force, do not enter child workspaces. Explicit parent operations
across a recorded mount boundary are refused.
Ordinary parent files, such as game/src/foo.cpp, remain editable with scribe edit and scribe submit. A directory becomes an independent repository only
when declared as a mount; the name game does not make it one. See the
practical walkthrough for single-repository edits
and changes that span the parent and engine.
Use project orchestration for root-level status, explicit
pin staging, and recoverable child-first publication. For independent submissions,
submit child changes first, then stage the parent pin with scribe project update
and submit that config change. There is no cross-repository atomic submission.
A child sync uses its saved mount pin by default; an explicit revision can be used when working
on the child independently. Parent sync refuses to silently rewind child work
submitted after the last applied pin; update the parent pin or reconcile the child
explicitly. Local modifications, opened work, and untracked files block repinning
or changing the child view. Interrupted child downloads remain resumable.
Deselecting or deleting a mount declaration retains its local directory. The
parent no longer contacts that child and continues to protect the directory from
parent scans. Removed declarations appear as retained in sync output. To reclaim
space or reuse a path, explicitly move the retained workspace outside the parent
once its work is safe. Changing repository or branch identity at an occupied mount
path also requires preserving that existing workspace elsewhere first. Historical
parent sync cannot overwrite files inside a physical child workspace.
A child with its own mount declarations is reported as unsupported before its files are synced; recursive mounts and cycles are not traversed. Mounting an existing repository does not create a fork. Cross-repository forks are outside the active roadmap; use branches within the child for its customizations.
SDK and web
The TypeScript SDK exposes client.mounts(parent, selection) for declarations
without target probes and client.resolveMount(declaration) for an independently
authorized target check. The latter returns available with repository/branch
names, or unavailable without those names. Operational failures propagate.
HTTP GET /api/v1/repositories/:repo/mounts?branch=…&revision=… returns
{ mounts: [...] }, with the same pin strings and permission states. It accepts
browser/public reads and PATs with contents:read. Responses use
Cache-Control: private, no-store; target authorization is not served from the
parent tree cache. Parent authorization failure remains an HTTP error.
The web browser shows virtual mount cards separately from parent files. Available cards link into the child at its branch ID and pinned revision using the current namespace routes. Unavailable cards have no link and reveal no fetched target name. Following a link performs the child's normal access checks again. The server still enforces mount boundaries for edits made through the SDK or web UI.