Multi-repo dependencies
MarkSpec can consume another repository’s requirements as a versioned git
dependency. The upstream repo’s entries hydrate into your project’s own
traceability graph, so a Satisfies: link can cross the repo boundary and
markspec check resolves it exactly like a same-project reference. This recipe
walks the consumer side end to end: declare the dependency, lock it, trace into
it, verify, and see each failure mode.
For the graph-integration semantics that make this work — how upstream entries join the graph, resolve, and gate collisions — see Upstream entries resolve in the graph in the CLI guide and ADR-031. This recipe is the how-to; that section is the reference.
1. Declare the dependency
Add a dependencies: entry to project.yaml naming the upstream repo’s git
URL:
# project.yaml
name: io.acme.braking-system
version: "2.3.0"
dependencies:
- url: git@github.com:acme/aeb-icd.git
name: icd # short id: cache dir, lock row, Origin badge
version: "v2.1.0" # exact tag → frozen baseline
url(required) — the upstream git repository.name(optional) — a short id used for the cache directory, the lockfile row, and theOrigin:badge on hydrated entries. Derived from the URL when absent (aeb-icd).version(optional) — the resolution intent:- absent → auto: the latest release tag, else the default-branch head.
- an exact tag (
v2.1.0) → a frozen baseline that never moves until you change it. - a branch name (
main) → tracks that branch’s head on every re-lock.
2. Lock it
markspec lock
markspec lock resolves the declared version intent against the upstream repo,
acquires the tree at the resolved commit via a shallow git fetch by sha (no
clone, no history), compiles it in-process, writes the compiled snapshot to
.markspec/cache/upstreams/icd/, and pins the result as an
[[upstream.dependency]] lockfile row. The cache directory is gitignored
automatically — lock appends .markspec/cache/ to .gitignore the first time
it runs.
The pinned row records the requested intent, what actually resolved, the exact commit, and the snapshot hash:
# markspec.lock (excerpt — @generated by `markspec lock`, do not hand-edit)
[[upstream.dependency]]
id = "icd"
url = "git@github.com:acme/aeb-icd.git"
intent = "v2.1.0"
resolved = "tag:v2.1.0"
sha = "9f3c1a2e5b7d0c4f6a81b2d3e4f5069718293a4b"
snapshot = "sha256:b1946ac92492d2347c6235b4d2611184..."
locked-at = "2026-07-05T12:00:00Z"
Only markspec lock touches the network. Once the pin exists, check,
compile, the LSP, and the MCP server all resolve entirely offline from the
cached snapshot — see lock in the CLI guide for the
first-lock / restore / update flows.
3. Trace across the repo boundary
Author a project entry whose trace value names an upstream display ID:
- [STK_BRK_0007] Deceleration request honored
The braking system shall action a deceleration request within 50 ms of
receiving it on the vehicle bus.
Id: 01ARZ3NDEKTSV4RRFFQ69G5FAV
Type: stakeholder-requirement
Satisfies: ICD_BRK_0010
ICD_BRK_0010 is declared in the upstream ICD repo, never in this project —
markspec lock hydrated it into the graph as a read-only, origin-tagged entry.
4. Verify
markspec check
exits 0. Satisfies: ICD_BRK_0010 resolves across the repo boundary with no
warning. The upstream entry is a full resolution target for show, context,
dependents, and report too:
markspec show ICD_BRK_0010 "docs/**/*.md"
prints the upstream entry with an Origin: line naming the upstream and its
version. The Source: line gives the entry’s location in the upstream repo’s
own tree — the path it compiled from, not a local cache path:
ICD_BRK_0010 Deceleration bus message contract
Type: interface
Shape: Authored
Origin: icd@v2.1.0
...
Source: docs/icd.md:12:1
5. Failure modes
| Symptom | Diagnostic | Fix |
|---|---|---|
Satisfies: names an id no upstream declares | MSL-T014 | Fix the trace value. The warning names every upstream searched, e.g. not found in project or upstreams: icd. |
A project entry reuses an upstream display ID or Id: | MSL-R014 | Rename the project entry — upstream entries are read-only and win the collision. |
| The cached snapshot is missing or its hash moved (fresh clone, cleaned tree, CI) | MSL-L212 | Run markspec lock — the restore flow re-fetches the pinned content without moving the pin. |
A dependencies: pin resolved to a branch or bare sha rather than a tag | MSL-L215 | Advisory by default; markspec check --strict promotes it to a hard error, so a release build cannot pass against an unbaselined dependency. Pin an exact tag to clear it. |
MSL-T014 replaces the plain MSL-L006 “unresolved reference” warning only
once the project declares any dependencies: or references: — a project with
no declared upstreams keeps the MSL-L006 behavior.
dependencies: vs references:
Both pin offline-after-lock under .markspec/cache/upstreams/<id>/, but they
model different relationships and are treated differently by coverage:
dependencies:— another project you build on (a git repository). Its entries participate in coverage like your own: a hydrated product requirement with no component coverage is a reported gap. Use this when you trace into the upstream and expect the traceability to be complete.references:— a published site you cite (a compile-output snapshot). Its entries are traceability leaves —report coveragenever reports one as an orphan or an unsatisfied gap, because a citation isn’t something your own project is expected to cover.
See Upstream entries resolve in the graph in the CLI guide for the authoritative coverage-treatment rule, and The projectRef shape for the full field reference shared by both lists.
Notes
- Shallow fetch, not a clone. Acquisition is a
git fetchat the resolved sha — no working clone, no history. Resolving an upstream from a forge release tarball (no git at all) is a planned fast-follow, not yet shipped. - Offline after lock. Only
markspec lockreaches the network.check,compile, the LSP, and the MCP server read the pinned cache and never fetch. - Upstream entries are read-only and validation-exempt. No structural checks or prose lint run against them — that already happened in their own repo — but they remain full resolution targets for cross-repo trace edges. See Upstream entries resolve in the graph for the full rule.
- Aggregate a whole program from one root. A root or program repo that depends on every member repo resolves the entire program’s cross-repo graph in one compile — see the Root/program project pattern in the CLI guide.
- Cache the snapshots in CI. Key an
actions/cacheonmarkspec.lockso a job only re-acquires an upstream when its pin actually moves.