Architecture
How spm's source is put together, for contributors. For the user-facing model, see How It Works; for the principles behind it, see Design Notes.
Module layering
Every top-level module under src/ and the modules it is allowed to depend on (a --> b: a uses b). cli is the top of the stack and the leaves at the bottom depend on nothing else in the crate. main is the entry point: it declares the modules and calls into cli, the only module allowed to.
The graph is pinned: tests/cli/architecture.rs derives the real module graph from the source and fails when it differs from its ALLOWED_EDGES table, when it has a cycle, or when the diagram below differs from that table. After a deliberate layering change, update ALLOWED_EDGES and regenerate the diagram with SPM_BLESS=1 cargo test module_graph_diagram (the failing test also prints the expected block).
Command flow
Every command that syncs a scope — add, remove, update, install, target add, link and unlink — runs the same path through src/cli/. It first opens a ScopeSession (src/cli/session.rs), the only way to take the scope's state lock, and holds it until its last write, so nothing it read can change underneath it. The shared sync pipeline (src/cli/sync/) then plans everything without writing outside the global store, and only a fully checked plan is applied.
spm target remove and spm clean also take a ScopeSession (Write) but bypass the sync pipeline: they resolve and fetch nothing, and remove spm's output from the targets themselves (target remove also rewrites ai.json), behind the same overlap guard (sync::guard_working_copies).
ai.jsonis written only after the plan succeeds, so a failedadd(an unreachable source, a bad ref, a blocked scan, …) leaves it untouched and the command can be retried.- Linked skills are recorded before they are written. When the links file changes,
applyfirst saves a pending one listing the skills planned from linked plugins alongside the previous ones, so a materialization that fails part-way never leaves spm-written skills unrecorded; the final links file narrows it once every target succeeded. ai.lockis written only after every target is materialized, so it never pins state that was not installed.--lock-onlyskips materialization and writesai.lockalone.spm doctorruns the same plan read-only and offline (LockMode::Check) and reports its verdict instead of applying it.
Install data flow
How a dependency travels from ai.json into the directory a tool loads it from, during the resolve and apply phases above.
- Resolve. Each direct dependency keeps its
ai.lockpin when its spec is unchanged; otherwiseresolverresolves the spec to an immutable commit SHA and store key (--lockednever re-resolves). - Fetch.
storemakes sure<repo>@<sha>is checked out in the global store, cloning it at most once per commit. A dependency overridden withspm linkis read from a private snapshot of its working copy instead. - Scan. Every skill and plugin passes the
scancontent gate before it can be materialized. WithresolveTransitiveon, each fetched skill's ownai.jsonis read too, and its dependencies go through the same resolve → fetch → scan steps (see Transitive Dependencies). - Materialize.
vendor::for_targetpicks the adapter for each target inai.json, which copies the scanned content where that tool loads it from — see Targets & Vendors.