# Versioned repository mounts

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:

```toml
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.

```sh
scribe workspace create orbit-game ./work --profile programmer
cd work
scribe workspace mounts
scribe sync
```

`workspace 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:

```sh
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 sync
```

If 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](/docs/repository-mounts.md#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](/docs/web-interface.md#virtual-repository-mounts).

## 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](/docs/tutorials/work-with-mounts.md) for single-repository edits
and changes that span the parent and engine.

Use [project orchestration](/docs/project-workflows.md) 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.
