src/orchestrator/projects.ts — the project-config load
Purpose
Section titled “Purpose”One code path for “which tasks exist, resolved”: prepareRun (a run,
a plan) and vx show both call loadProjects, so what show prints
is what a run would see — the plugin config and project stages
included. Before this, show read config files raw and printed
(no vx config) for a package turbo() gives tasks to.
Public surface
Section titled “Public surface”export function loadWorkspacePlugins( workspaceRoot: string, warn: (m: string) => void,): Promise<{ workspaceConfig: WorkspaceConfig | null; plugins: readonly VxPlugin[] }>
export interface LoadProjectsArgs { workspaceRoot: string cacheDir: string plugins: readonly VxPlugin[] projectMetas: readonly ProjectMeta[] packageGraph: PackageGraph seeds: 'all' | Iterable<string> // unknown / config-less names ignored closure: boolean // also load each seed's transitive package deps lock: Lockfile | null // read from the lock instead of evaluating evalCache: LoadProjectConfigOptions['evalCache'] warn: (m: string) => void staged?: ReadonlyMap<string, ProjectEntry> // entries a load in this process already produced}export interface LoadedProjects { projects: Map<string, ProjectEntry> configured: readonly ProjectMeta[] // every project that can carry tasks}export function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjects>
/** A reader's view: every stage applied, cached evaluations served, no closure, no lock. */export function loadResolvedProjects( workspaceRoot: string, opts?: { scope?: 'all' | readonly string[]; warn?: (message: string) => void },): Promise<Map<string, ProjectEntry>>Algorithm
Section titled “Algorithm”- Who can carry tasks. A project with a config file; and, when
any plugin declares
project, every package — it loads as{ tasks: {} }for the stage to fill.configuredis that set; boundary geometry fences all of them, loaded or not. - Seeds.
'all', or names filtered toconfigured. Withclosure, each seed’spackageGraph.transitiveDepsjoin — that bounds^taskfrontier expansion. The closure is skipped when every configured project is already pending. - Rounds to a fixpoint. Each round evaluates its config files in
one
loadProjectConfigsbatch (or reads them from the lock), applies theprojectstage per project, re-validating after EACH plugin under an(after plugin '<name>')label, then queues any project apkg#taskdependsOn entry names — the package graph cannot see the cross form. No cross deps → one round. A project present instagedis taken as is — no evaluation, no stage — and still contributes its cross deps; seeding and scoping do not change. The CLI’s selection pass (resolveFilters→taskEdges) is the producer: before it, a graph-walking filter put every config through theprojectstage twice per run (tests/staged-once.test.ts).
prepareRun passes closure: true and the run’s lock and eval cache.
vx show, vx watch’s config sweep and the CLI’s selection pass load
through cli/workspace-config.ts, which passes closure: false, the
lock only under --frozen, and a local cache opened for the
evaluations alone. loadResolvedProjects is the same read for an
embedder — vx mcp’s tools and @vzn/vx-schedule-history call it, and
@vzn/vx exports it — with discovery and the plugin load folded in:
scope is every project or a list of names, no closure, no lock, and a
cache opened and closed around the load.
tests/prepare-run.test.ts,tests/plugin-pipeline.test.ts— the run path (scope, closure, cross deps, theprojectstage).tests/show-info.test.ts—showthrough the same load: a config-less package under aprojectplugin shows its tasks.