Skip to content

Multi-repository workflow

Audience: project maintainers and authorized agents configuring an installation. For the everyday workflow, start with Quickstart.

Use this guide when one change writes code in several repositories, or when a test runs in one repository but needs code or contracts from others.

If the change needs only one repository, stop here: the normal Quickstart already covers the simpler path.

A scenario: one change across an API repo and a web repo

Section titled “A scenario: one change across an API repo and a web repo”

The project declares its repositories, the change selects which to write or read, and one proof and one Land cover them all.

You ask for a new field on the profile page. The API repository must return it, and the web repository must show it. A shared contracts repository describes the response shape and must not be edited.

The decision: declare scope first. Before Build, say (or confirm) which repositories this change may write (api, app), which it may only read (contracts), and what must happen first (api before app). Change Loop will not touch, test against, or deliver any repository outside that declaration.

What you see:

  • The agent’s statement of scope before you approve: repositories written, repositories read, and their order.
  • Build and Prove progress per repository. The web checks run against the API’s new, not-yet-delivered code, because both live in isolated sandboxes.
  • At the end, a result that names which repositories were read, written, proven, and still waiting to Land.

What you decide:

  1. Confirm the repository scope and order (the one consequential choice).
  2. Approve the agreement, as for any change.
  3. Say /land once. Every writable repository then receives its changes as an uncommitted diff; Git HEAD and index stay unchanged everywhere. You review each diff and commit through your normal Git process. Only archived means done; proven does not.

If a read repository moves while you work, the agent re-syncs and re-proves automatically. You do not run sandbox or sync commands.

Everything below is the mechanics the agent and project maintainers use.

These files answer different questions. Configure them in this order:

Order Contract Question it answers
1 openspec/repositories.yaml Which repositories may this project use?
2 openspec/changes/<change>/repositories.yaml Which of them may this change read or write?
3 derived provider or conditional execution.yaml fields Which repositories does this evidence command need?

Do not skip directly to provider wiring. A provider cannot prove a repository that the project topology does not know or the change did not select.

Add every repository Change Loop may isolate to openspec/repositories.yaml:

{
"version": 1,
"repositories": [
{ "id": "api", "path": "services/api", "setupCommand": "npm ci" },
{ "id": "app", "path": "apps/web", "setupCommand": "npm ci" },
{ "id": "contracts", "path": "contracts", "mode": "read" },
{
"id": "partner-sdk",
"type": "external",
"path": "../partner-sdk",
"mode": "read",
"allowOutsideRoot": true
}
]
}

What matters:

  • id is the stable name used by tasks, providers, receipts, and Land.
  • path is relative to the control repository.
  • setupCommand prepares that repository’s newly created worktree.
  • a path outside the control repository, such as a sibling ../sdk, must declare allowOutsideRoot: true. Its type may stay git (the default) or be external; both behave the same. A writable sibling owns tasks and Lands like a submodule;
  • every selected repository must already be an initialized Git repository.

Change Loop refuses a non-Git dependency because it cannot pin or isolate a moving directory honestly.

Check the result before creating a change:

Terminal window
claude-foundation repos

Inside the change, select only the repositories it needs:

{
"version": 1,
"repositories": [
{ "id": "api", "mode": "write", "dependsOn": [] },
{ "id": "app", "mode": "write", "dependsOn": ["api"] },
{ "id": "contracts", "mode": "read", "dependsOn": [] },
{ "id": "partner-sdk", "mode": "read", "dependsOn": [] }
]
}

Use write only where the change will produce code. Use read for test data, contracts, SDKs, and integration dependencies. A read repository is part of proof identity, but it is not a Build or Land target.

Selecting any non-root repository makes the lifecycle composite. This includes a selection with exactly one child and no root product work; it must not be downgraded to the single-root Apply or archive path.

Then inspect the resolved selection:

Terminal window
claude-foundation repos <change>

Every implementation task names its owning repository and paths. Dependencies make cross-repository order explicit:

- [ ] **T001** Update API [repo:api] [kind:implementation] [paths:src/profile]
- [ ] **T002** Update app [repo:app] [kind:implementation] [depends:T001] [paths:src/profile]
- [ ] **T003** Verify contract [repo:app] [kind:contract] [depends:T001,T002]

Change Loop compiles repository selection, tasks, providers, and Land order into the execution graph. Do not create a second graph file.

The normal agent command creates the selected workspaces together:

Terminal window
claude-foundation advance <change> --through build

sandbox create --all and sandbox inspect remain operator diagnostics under help --all; users do not need to sequence them.

After isolation, every selected child must have a runtime record matching its catalog target, access mode, base head, and live worktree. sandbox inspect lists missing-record, unexpected-record, missing, or invalid instead of silently substituting the target checkout. Inspection reads filesystem and Git metadata directly, so a repository-controlled git on PATH is never run.

Write repositories receive isolated Build worktrees. Git-backed read and external repositories receive pinned detached worktrees. The command does not make an external service or arbitrary folder safe; this is Git workspace isolation, not an OS security boundary.

Root code and tests can still use a nested repository through its root path, for example require('./packages/lib'). In the root sandbox the harness links a selected repository’s path to that repository’s sandbox, so root checks see its uncommitted work and an edit through either path lands in the same place. An unselected nested repository holds the commit the root gitlink records, checked out from local objects even when the target never initialized it, and is read-only. These paths are never root changes: they stay out of root proof, review, the changed surface, and Land. Nobody needs to create a link or initialize a submodule by hand.

For custom wiring in conditional execution.yaml, repository is the provider’s working directory. repositories is the complete set the command reads:

{
"providers": {
"integration": {
"capability": "integration",
"adapter": "command",
"repository": "api",
"repositories": ["api", "app", "contracts", "partner-sdk"],
"command": ["npm", "run", "test:integration"]
}
}
}

Change Loop passes two relevant environment variables:

  • FOUNDATION_REPOSITORY_ID — the working-directory repository ID;
  • FOUNDATION_REPOSITORIES_FILE — a versioned JSON manifest mapping every scoped ID to its isolated path, access, and baseHead.

Changed-surface checks, review packets, provider manifests, snapshots, and Land use this same recorded base. Before isolation the selected source head may seed the record; after isolation a missing child binding is infrastructure failure.

Provider code must read the manifest. It must not assume all five company repositories happen to be checked out as siblings on the current machine.

The repository set is part of command deduplication and receipt identity. Two identical commands with different repository sets execute separately, and each receipt records its complete repositoryIds.

Plan parallel workers only after scope and dependencies are stable:

Terminal window
claude-foundation agents plan <change>

If another change advances a selected repository, advance synchronizes the sandbox itself before Prove; sandbox sync <change> is the diagnostic primitive behind it. Sync refreshes child read worktrees even when the control sandbox uses copy mode. A repository setupCommand runs again after refresh. If setup or a provider leaves a tracked change in a read workspace, readiness fails closed.

Terminal window
claude-foundation advance <change> --through proven

proof readiness and proof run remain diagnostic primitives. Prove may run independent branches concurrently and preserve completed branches after a failure. Aggregate proof still requires every selected repository and provider scope to match the current graph. A moved read dependency requires sync and fresh proof; Change Loop never certifies the old commit under the new repository manifest.

Consumer quality follows the same graph but never averages repositories. Add a row for every selected repository to quality/foundation-quality.json; a selected repository missing from that file fails closed. Each lane keeps its own commit, workspace digest, tool/config identity, baseline, and assurance. Use quality run --change <change> before Prove or let evidence bootstrap wire the committed quality config as static-analysis evidence. See Consumer quality gates.

Read repositories have no Land node. One explicit user command grants the complete local delivery transaction:

/land <change>

The harness prepares every writable target before the first write, then applies the explicitly authorized projections in dependency order with durable checkpoints. Each node finishes applied-uncommitted; Git HEAD and the index remain unchanged in every repository, and existing non-overlapping edits are preserved. A later /land resumes the same transaction and skips verified nodes. land check, legacy land record, and land resume remain diagnostic/compatibility primitives—not steps the user must sequence. Commits, pushes, and pull requests stay outside Land under separate user authority.

The saga is also required when exactly one non-root child is selected. A missing child runtime record cannot make Land take the single-repository shortcut.

What happened Correct next action
A selected target moved advance synchronizes the sandbox and proves again
Sync reports a replay conflict The agent resolves the named paths in the sandbox; do not recreate the change
A read repository is dirty Remove the mutation or fix setup/provider behavior
Repository setup failed Harness retries only that repository and preserves ready siblings; change policy only if the declared command itself is wrong
A selected child binding is missing Harness repairs the binding while preserving valid worktrees; use sandbox inspect only for requested diagnosis
A canonical child path belongs to another repository Keep it untouched, inspect the reported path, then correct the target/path conflict or explicitly abandon the change
Provider cannot see a repository Add it to provider repositories; do not hard-code a local path
A check refuses because an unselected repository’s root copy changed Revert that copy (the refusal names the command), or select the repository through a semantic amendment
Land is interrupted Invoke /land <change> again; it resumes the journal and re-applies root or repository sandbox work added after an earlier delivery
Land stops with ROOT_POINTER_MOVED The root sandbox moved the pointer of a selected submodule: the agent brings the commit into that repository’s sandbox, restores the root pointer, and resumes
Land stops with repository-pointer-change The root sandbox moved the pointer of an unselected submodule: the user chooses to land it through that repository, restore the pointer and land the rest, or pause

User: confirm consequential repository scope/dependency semantics, invoke Land once, review the final diffs, and commit through the project’s normal Git process. You do not construct manifests, grants, receipts, hashes, or journals.

Agent: configure the three scopes in order, use the manifest paths, run readiness before spending a Prove attempt, follow the reported recovery, and state which repositories were read, written, proven, and still waiting to Land. Do not ask the user to copy protocol JSON or silently reduce a five-repository test to the three repositories present in the sandbox.