src/orchestrator/plan.ts — --dry / --graph planning
Purpose
Section titled “Purpose”Walk the task graph, compute every task’s cache key, and probe the
cache to predict what vx run would do — without executing. Drives
the --dry, --dry=json, and --graph output formats.
Public surface
Section titled “Public surface”export type CacheStatus = | 'hit-local' // entry exists locally | 'hit-remote' // remote layer has it (would be fetched) | 'miss' // caching enabled but no entry | 'no-cache' // task opts out (no `cache` block) or --no-cache | 'group' // no `exec`; aggregator only
export interface PlannedTask { node: TaskNode hash: string cacheStatus: CacheStatus deps: readonly string[]}
export interface RunPlan { tasks: PlannedTask[]}
export interface PlanArgs { nodes: Map<string, TaskNode> workspaceRoot: string workspaceFingerprint: string cache: CacheLayer noCache: boolean forwardArgs?: readonly string[] nestedDirsByProject: Map<string, string[]>}
export function plan(args: PlanArgs): Promise<RunPlan>Algorithm
Section titled “Algorithm”Piggybacks on runGraph with concurrency: 1 and a planning
execute closure:
- For each node in topo order, compute the same cache key the real
run would (
computeTaskHashfor normal tasks,computeGroupHashfor groups). - Probe
cache.get(hash):cache.getreturns a hit object →'hit-local'(or'hit-remote'whenhit.source === 'remote').- Returns
null→'miss'.
- Group tasks short-circuit to
'group'. - Tasks with no
cacheblock OR--no-cacheset →'no-cache'. - Return a
RunPlanwith onePlannedTaskper node, preserving theTaskNode.depsgraph for downstream rendering.
Side effects
Section titled “Side effects”cache.get(hash) bumps the SQLite accessed_at column for every hit
(that’s the contract). Otherwise the planner is read-only — no spawn,
no cleanOutputs, no restoreOutputs.
Why concurrency: 1
Section titled “Why concurrency: 1”Planning is fast (no spawn, just a hash + DB lookup) and sequential
is easier to reason about. Topological ordering is what the
underlying runGraph provides; we keep it. Parallel planning could
shave milliseconds; not worth the complexity.
tests/plan-format.test.ts covers the formatters consuming
RunPlan; tests/orchestrator.test.ts covers planRun end-to-end
(workspace discovery + graph + planner). Specific cases:
--no-cacheflips every task to'no-cache'.- Local cache hit predicted correctly after a real run.
- Remote hit prediction (LayeredCache + remote stub).
- Group task status.
- Empty plan when no projects declare the task.
Replacing this module
Section titled “Replacing this module”This is a thin wrapper around computeTaskHash + cache.get. If you
need a richer plan (e.g. include estimated duration from runs
history), extend PlannedTask and update the formatters.