API reference
Every export of @vzn/vx, generated from src/index.ts by
tests/api-reference.test.ts: the declaration (comments dropped, a
function to its signature, a class to its public members) and the doc
comment above it. The public surface groups the
same names by what they are for; the plugins guide shows them in use.
AdmitContext
Section titled “AdmitContext”type · src/orchestrator/plugin.ts
export interface AdmitContext { readonly running: readonly TaskNode[] readonly concurrency: number}applyMigration
Section titled “applyMigration”function · src/workspace/migration.ts
Render a plan to files, refuse to overwrite without force, write (or
print, under dry), and report. Returns the process exit code.
export async function applyMigration(args: ApplyMigrationArgs): Promise<number>ApplyMigrationArgs
Section titled “ApplyMigrationArgs”type · src/workspace/migration.ts
export interface ApplyMigrationArgs { root: string metas: readonly ProjectMeta[] plan: MigrationPlan source: string verb: string dry: boolean force: boolean init?: boolean notes?: readonly string[] format?: MigrationFormat}buildPackageGraph
Section titled “buildPackageGraph”function · src/workspace/package-graph.ts
taskEdges: project → the projects its tasks name in a cross-project
dependsOn (e2e → app from dependsOn: ['app#build']). A dependent
the manifest does not know but the task graph does; without it
--filter '...app' never selected e2e, and a CI that runs “what
changed and everything depending on it” silently left it out
(2026-09-10). The ^task walk still reads directDeps, which carries
both — a task edge IS a dependency.
export function buildPackageGraph( projects: ProjectMeta[], taskEdges?: ReadonlyMap<string, readonly string[]>,): PackageGraphclass · src/cache/cache.ts
export class Cache implements CacheLayer { readonly hasRemote readonly schemaReset: SchemaReset | null readonly formatChange: SchemaReset | null static inspect(cacheDir: string): Cache static async orphansBeforeReset( cacheDir: string, ): Promise<{ found: string; orphans: number; orphanBytes: number } | null> constructor( private readonly cacheDir: string, localPolicy: { read: boolean; write: boolean } = { read: true, write: true }, repoDir?: string, private readonly artifactCeiling: number = MAX_DECOMPRESSED_ARTIFACT_BYTES, mode: 'open' | 'inspect' = 'open', ) getConfigEval(key: string): string | null getConfigClosures(configPaths: readonly string[]): Map<string, string[]> putConfigClosure(configPath: string, files: readonly string[]): void getConfigEvals(keys: readonly string[]): Map<string, string> putConfigEval(key: string, json: string): void putConfigEvals(entries: ReadonlyArray<readonly [string, string]>): void putConfigClosures(entries: ReadonlyArray<readonly [string, readonly string[]]>): void hashFile(filePath: string): Promise<string> hashBytes(bytes: Uint8Array, nearPath: string): string hashFiles(paths: readonly string[]): Promise<Map<string, string>> key(input: CacheKeyInput): Promise<string> async get(hash: string, _ctx?: CacheGetContext): Promise<CacheEntry | null> getIngested(hash: string): Promise<CacheEntry | null> async getMany(hashes: readonly string[]): Promise<Map<string, CacheEntry>> async has(hash: string): Promise<'local' | 'remote' | null> async prefetch(_hash: string, _ctx?: CacheGetContext): Promise<boolean> loadOutputFilesBatch(hashes: readonly string[]): Map<string, OutputFileRow[]> isOutputsCurrent(projectDir: string, expected: readonly OutputFileRow[]): Promise<boolean> outputsPath(hash: string): string async recordOutputDirs( hash: string, projectDir: string, prefixes: readonly string[], holds?: (files: readonly string[]) => boolean, ): Promise<void> recordOutputStamps(hash: string, projectDir: string, workspaceRoot: string): void loadOutputDirsBatch(hashes: readonly string[]): Map<string, OutputDirRow[]> outputDirsCurrent(projectDir: string, rows: readonly OutputDirRow[]): Promise<boolean> async restoreOutputs(hash: string, projectDir: string, workspaceRoot?: string): Promise<void> assertWritable(): void async save(args: { hash: string entry: Omit<CacheEntry, 'hash' | 'storedAt' | 'outputFiles' | 'exitCode'> projectDir: string outputFiles: string[] skipLocalWrite?: boolean workspaceOutputFiles?: string[] workspaceRoot?: string inputComponents?: readonly TaskInputRow[] }): Promise<void> get localWritesEnabled(): boolean packArtifactBytes(args: SaveArgs): Promise<Uint8Array> async ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void> dbHandle(): Database recordRun(run: RunRecord): void recordRuns(runs: readonly RunRecord[]): void recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void stats(opts: CacheStatsOptions = {}): CacheStats async evictIfDue( policy: { maxAgeMs?: number; maxBytes?: number }, now: number = Date.now(), ): Promise<PruneResult | null> async prune(options: PruneOptions): Promise<PruneResult> async orphanStats(): Promise<{ orphans: number; orphanBytes: number }> close(): void}CacheConfig
Section titled “CacheConfig”type · src/config.ts
export interface CacheConfig { inputs: CacheInputs outputs: CacheOutputs}CacheContext
Section titled “CacheContext”type · src/orchestrator/plugin.ts
export interface CacheContext extends BaseContext { readonly localCache: Cache readonly policy: CachePolicy}CacheInputs
Section titled “CacheInputs”type · src/config.ts
export interface CacheInputs { files: string[] workspaceFiles?: string[] env?: string[] tasks?: readonly string[] runtime?: string[] workspaceRuntime?: string[]}CacheLayer
Section titled “CacheLayer”type · src/cache/layer.ts
The shape every cache implementation honors. Cache (the local v10
implementation) and LayeredCache both implements this so the
orchestrator’s executeTask can take either without a discriminated
union and we get a compile-time guarantee the surfaces stay congruent.
export interface CacheLayer { readonly local?: Cache | undefined readonly hasRemote?: boolean remoteHasMany?(hashes: readonly string[]): Promise<Set<string> | null> markRemoteAbsent?(hashes: Iterable<string>): void drainUploads?(): Promise<void> key(input: CacheKeyInput): Promise<string> get(hash: string, ctx?: CacheGetContext): Promise<CacheEntry | null> getMany?(hashes: readonly string[]): Promise<Map<string, CacheEntry>> has(hash: string): Promise<'local' | 'remote' | null> prefetch(hash: string, ctx?: CacheGetContext): Promise<boolean> loadOutputFilesBatch(hashes: readonly string[]): Map<string, OutputFileRow[]> isOutputsCurrent(projectDir: string, expected: readonly OutputFileRow[]): Promise<boolean> recordOutputDirs?( hash: string, projectDir: string, prefixes: readonly string[], holds?: (files: readonly string[]) => boolean, ): Promise<void> recordOutputStamps?(hash: string, projectDir: string, workspaceRoot: string): void loadOutputDirsBatch?(hashes: readonly string[]): Map<string, OutputDirRow[]> outputDirsCurrent?(projectDir: string, rows: readonly OutputDirRow[]): Promise<boolean> restoreOutputs(hash: string, projectDir: string, workspaceRoot?: string): Promise<void> save(args: { hash: string entry: Omit<CacheEntry, 'hash' | 'storedAt' | 'outputFiles' | 'exitCode'> projectDir: string outputFiles: string[] skipLocalWrite?: boolean workspaceOutputFiles?: string[] workspaceRoot?: string inputComponents?: readonly TaskInputRow[] }): Promise<void> ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void> recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void stats(opts?: CacheStatsOptions): CacheStats hashFile(filePath: string): Promise<string> outputsPath(hash: string): string prune(options: PruneOptions): Promise<PruneResult> close(): void}CacheOutputs
Section titled “CacheOutputs”type · src/config.ts
export interface CacheOutputs { files: string[] workspaceFiles?: string[]}CachePolicy
Section titled “CachePolicy”type · src/cache/policy.ts
Independent read/write control over the two cache layers (local +
remote). Replaces the old single noCache boolean: each axis can be
toggled on its own so --force (re-execute but still refresh the
cache) is distinct from --no-cache (disable everything).
Only the task-artifact get/save path is gated. recordRun, stats,
prune, key derivation, and prefetch-ingest are never affected — they
are bookkeeping/analytics that a run policy has no business disabling.
export interface CachePolicy { localRead: boolean localWrite: boolean remoteRead: boolean remoteWrite: boolean}CacheSource
Section titled “CacheSource”type · src/orchestrator/telemetry.ts
Where a task’s result came from, derived ONCE in core from the status.
export type CacheSource = 'miss' | 'local' | 'remote' | 'none'CiContext
Section titled “CiContext”type · src/orchestrator/run-context.ts
export interface CiContext { ci: boolean provider: string | null}clampInt
Section titled “clampInt”function · src/util/num.ts
Clamp to an INTEGER in [min, max]; a non-finite value collapses to min.
The floor is load-bearing wherever the result reaches SQL: a fractional
LIMIT is a datatype mismatch error, not a smaller page.
export function clampInt(n: number, min: number, max: number): numbercollectInfo
Section titled “collectInfo”function · src/orchestrator/doctor.ts
export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): Promise<InfoFacts>CollectInfoOptions
Section titled “CollectInfoOptions”type · src/orchestrator/doctor.ts
export interface CollectInfoOptions { readonly cacheDir?: string readonly warn?: (message: string) => void}CommandContext
Section titled “CommandContext”type · src/orchestrator/plugin.ts
export interface CommandContext extends BaseContext { readonly concurrency: number}definePlugin
Section titled “definePlugin”function · src/orchestrator/plugin.ts
The one way to make a plugin: definePlugin(import.meta, { ...hooks }).
The name is read from the package the calling module belongs to — the
nearest package.json above it — and stamped where the workspace loader
checks for it, so a plugin cannot be named anything but its package.
export function definePlugin(origin: PluginOrigin, hooks: PluginHooks): VxPlugindefineProject
Section titled “defineProject”function · src/config.ts
Identity function — exists only so TypeScript narrows literal types
and, crucially, validates dependsOn against this project’s own
task names. A bare entry that isn’t a declared task key is a compile
error; ^name / pkg#name forms reference other projects and stay
free strings. Runtime behavior is unchanged (it returns its input).
export function defineProject<const T extends ProjectConfig>( config: T & { tasks?: { [K in keyof NonNullable<T['tasks']>]?: { dependsOn?: readonly DependsOnEntry<Extract<keyof NonNullable<T['tasks']>, string>>[] } } },): TdefineWorkspace
Section titled “defineWorkspace”function · src/config.ts
export function defineWorkspace<T extends WorkspaceConfig>(config: T): TescapeMarkdownCell
Section titled “escapeMarkdownCell”function · src/orchestrator/run-report.ts
Make a value safe inside a GFM table cell. Task names are arbitrary TS
object keys and the loader accepts | and newlines, either of which
silently breaks the table on the consumer (>> $GITHUB_STEP_SUMMARY):
a bare pipe adds a column, a newline splits the row.
Exported because this file is not the only markdown table describing a run:
the cloud plugin’s GitHub job summary renders the same data from the same
unvalidated names and shipped WITHOUT this escape, so a | in a task name
or an output path shifted its columns. One definition, so the two
cannot disagree about what a cell may contain.
export function escapeMarkdownCell(value: string): stringExecConfig
Section titled “ExecConfig”type · src/config.ts
export interface ExecConfig { command: string remote?: boolean | 'only' env?: ExecEnv timeout?: number retries?: number persistent?: PersistentConfig sandbox?: SandboxConfig}ExecEnv
Section titled “ExecEnv”type · src/config.ts
export interface ExecEnv { passThrough?: string[] define?: Record<string, string> secret?: string[]}ExecuteRequest
Section titled “ExecuteRequest”type · src/exec/executor.ts
export interface ExecuteRequest { readonly taskId: string readonly workspaceRoot: string readonly inputs?: TaskInputs readonly cacheKey?: string readonly refresh?: boolean readonly remoteOnly?: boolean readonly download?: 'eager' | 'deferred' readonly outputs: { readonly files: readonly string[] readonly workspaceFiles: readonly string[] } readonly command: string readonly forwardArgs: readonly string[] readonly cwd: string readonly env: NodeJS.ProcessEnv readonly envDefine: Readonly<Record<string, string>> readonly capture: CaptureConfig readonly timeoutMs?: number readonly onStdout: (chunk: string) => void readonly onStderr: (chunk: string) => void readonly signal?: AbortSignal readonly liveChildren?: Set<ReturnType<typeof Bun.spawn>> readonly sandbox?: ExecuteSandbox}ExecuteResult
Section titled “ExecuteResult”type · src/exec/executor.ts
export interface ExecuteResult extends RunResult { readonly violations: readonly SandboxViolation[] readonly outputs?: { kind: 'disk' } | { kind: 'deferred'; materialize: () => Promise<void> } readonly where?: string}ExecuteSandbox
Section titled “ExecuteSandbox”type · src/exec/executor.ts
Sandbox baselines + the user’s resolved sandbox block, when the task is sandboxed.
export interface ExecuteSandbox { readonly baseAllowRead: readonly string[] readonly baseDenyRead: readonly string[] readonly reportWithin: string readonly reportLinked: readonly string[] readonly config: ResolvedSandboxConfig}ExecutorContext
Section titled “ExecutorContext”type · src/orchestrator/plugin.ts
export interface ExecutorContext extends BaseContext { readonly concurrency: number}exitSignal
Section titled “exitSignal”function · src/exec/runner.ts
The reverse: the signal an exit above 128 stands for (137 → SIGKILL), by the platform’s numbering; undefined for a plain exit. The shell reports 128 + n for a death by signal n, so the read is the shell’s convention, not proof — a command may exit 137 on its own.
export function exitSignal(code: number): string | undefinedfindWorkspaceRoot
Section titled “findWorkspaceRoot”function · src/workspace/workspace.ts
Walk up from start to find the workspace root. A directory is a root
CANDIDATE when it contains pnpm-workspace.yaml or a package.json.
The nearest candidate that CLAIMS start wins — one of the directories
between it and start matches one of its package globs. Every workspace
member has its own package.json, so stopping at the first candidate would
make a run from inside a package treat that package as the whole workspace:
^task edges vanish, upstream hashes drop out of the cache key (stale
hits), and a second cache dir appears under the member. Claiming is decided
with the same globs loadWorkspace applies, so “the root that claims me”
and “the root that lists me as a project” cannot diverge.
When no candidate claims start — a standalone package, or a subdirectory
of a single-project repo — the nearest candidate wins (the root itself IS
the project). Throws a UserError when there is no candidate before /.
A load that goes on to loadWorkspace passes its reads, so the
root’s manifest is read once for both.
export async function findWorkspaceRoot( start: string, reads: LoadReads = new Map(),): Promise<string>FingerprintChange
Section titled “FingerprintChange”type · src/orchestrator/plugin.ts
export interface FingerprintChange { readonly file: string readonly before: Uint8Array | null readonly after: Uint8Array | null}FingerprintClaim
Section titled “FingerprintClaim”type · src/orchestrator/plugin.ts
A plugin’s claim on workspace fingerprint files — see VxPlugin.fingerprint.
export interface FingerprintClaim { readonly files: readonly string[] affected( change: FingerprintChange, ctx: FingerprintContext, ): Iterable<string> | undefined | Promise<Iterable<string> | undefined>}FingerprintContext
Section titled “FingerprintContext”type · src/orchestrator/plugin.ts
export interface FingerprintContext extends BaseContext { readonly projects: ReadonlyArray<{ readonly name: string; readonly dir: string }>}foldScriptHooks
Section titled “foldScriptHooks”function · src/workspace/migration.ts
A package.json script with the pre<name> / post<name> hooks npm runs
around it, as ONE sh command. Each part runs in its own subshell, so a
; or an exit in one ends that part alone, and the chain stops at the
first that fails, as npm stops. The parts sit in a function the
forwarded -- args are appended to, and only the body takes them, as
npm appends them to the script and never to its hooks. A plain &&
join handed them to the post hook, and test -f x && echo A; echo B ran
echo B after a failed pre hook and went green (item 905). Each part
ends on its own line, so a trailing # comment cannot swallow the
paren. A script with no hooks is its body, verbatim.
export function foldScriptHooks( pre: string | undefined, body: string, post: string | undefined,): stringGeneratedProject
Section titled “GeneratedProject”type · src/workspace/migration.ts
export interface GeneratedProject { name: string dir: string importLines: string[] tasks: GeneratedTask[]}GeneratedTask
Section titled “GeneratedTask”type · src/workspace/migration.ts
export interface GeneratedTask { name: string todos: string[] task: Record<string, unknown> | null}GitContext
Section titled “GitContext”type · src/orchestrator/run-context.ts
export interface GitContext { commitSha: string | null branch: string | null dirty: boolean | null}GraphHookContext
Section titled “GraphHookContext”type · src/orchestrator/plugin.ts
export interface GraphHookContext extends BaseContext { readonly requested: readonly string[]}HistoryProvider
Section titled “HistoryProvider”type · src/orchestrator/history.ts
export interface HistoryProvider { loadFor(taskIds: readonly string[]): Promise<HistoryTable>}HistoryTable
Section titled “HistoryTable”type · src/orchestrator/history.ts
Map keyed by project#task.
export type HistoryTable = ReadonlyMap<string, TaskHistory>HostContext
Section titled “HostContext”type · src/orchestrator/run-context.ts
export interface HostContext { host: string | null os: string arch: string}InfoFacts
Section titled “InfoFacts”type · src/orchestrator/doctor.ts
The doctor’s facts, typed: what --format json prints and the pretty rows render.
export interface InfoFacts { vx: string bun: string bunSupported: boolean git: string | null gitStatusCache: { fsmonitor: boolean; untrackedCache: boolean } | null workspaceRoot: string projects: number tasks: number configErrors: Array<{ path: string; message: string }> plugins: Array<{ name: string; seams: string[] }> workers: { count: number source: 'workspace' | 'cgroup' | 'cores' cores: number cpuQuota: number | null } memory: { usableBytes: number; totalBytes: number; cgroupLimitBytes: number | null } cacheDir: string cacheVersion: string schemaVersion: string cacheEntries: number cacheBytes: number orphans: { artifacts: number; bytes: number } runs24h: number hits24h: number flakyTasks: FlakyTask[] lockfile: boolean sandbox: { available: boolean; reason: string; declared: number }}InvocationRecord
Section titled “InvocationRecord”type · src/cache/layer.ts
One header row per vx run invocation (the invocations table). All
fields mirror the columns; nullable VCS/host columns are null when
the probe failed (not a git repo, hostname unavailable). Recorded
once per run inside the same transaction as the per-task runs rows.
export interface InvocationRecord { runId: string command: string requestedTasks: string cachePolicy: string concurrency: number flow: 'focused' | 'broad' | null startedAt: number endedAt: number totalDurationMs: number taskCount: number failedCount: number hitCount: number hitLocalCount: number hitRemoteCount: number exitOk: boolean commitSha: string | null branch: string | null dirty: boolean | null ci: boolean ciProvider: string | null host: string | null os: string | null arch: string | null vxVersion: string tags: string}isCacheHit
Section titled “isCacheHit”function · src/orchestrator/telemetry.ts
Did the task’s result come out of the cache (either layer)? Derived from
deriveCacheSource rather than re-listing the two hit statuses, so the two
cannot disagree about what a hit is. Unknown strings read as not-a-hit.
export function isCacheHit(status: string): booleanisLiteralPattern
Section titled “isLiteralPattern”function · src/util/paths.ts
True when a pattern carries no wildcard — it names exactly one path.
The character SET is the whole content: in a task glob *, ? and a
brace alternation are wildcards, so a pattern holding any of them must
be MATCHED, never compared as a string. It lives here, exported, because
four places asked the same question and one of them asked it with a
smaller set: graph/task-graph.ts omitted {}, so dist/{a,b}.txt
counted as a literal and the overlapping-output refusal compared it to
dist/a.txt as two unequal strings — the two tasks were accepted and
then deleted each other’s outputs, green, every run (item 495). That is
the same divergence asTrees was moved here to end in item 442, and the
same one that removed @vzn/vx-migrate’s copy of outputsOverlap in
item 445.
export function isLiteralPattern(glob: string): booleanisPassStatus
Section titled “isPassStatus”function · src/orchestrator/telemetry.ts
Did the task pass? A cache hit counts — it produced the same result without
spending the time, which is the whole point. skipped and aborted do NOT:
neither finished on its own terms, so neither can vouch for anything.
Takes string, not TaskStatus, because most callers hold a status that
arrived over a wire or out of a database column. An unrecognised string
reads as NOT passing — the safe direction, since the alternative is calling
a run green on a status this build has never heard of.
export function isPassStatus(status: string): booleanisUserError
Section titled “isUserError”function · src/util/errors.ts
instanceof UserError, plus the same class arriving from ANOTHER COPY of
core. A compiled vx binary carries core inside it while a plugin in the
workspace imports @vzn/vx from node_modules, so a plugin’s UserError
is a different class object and instanceof is false — a plugin verb’s
“bad flag —x” printed as UserError: bad flag --x with a stack, and a
REAPI refusal would have read as an “internal error” (reproduced through
the real binary, 2026-09-03). The name is the contract that survives the
copy boundary.
export function isUserError(err: unknown): err is UserErrorKeyHookContext
Section titled “KeyHookContext”type · src/orchestrator/plugin.ts
export interface KeyHookContext extends BaseContext {}latestRunId
Section titled “latestRunId”function · src/orchestrator/metrics.ts
The latest recorded run of a task (run_id may be NULL on very old rows).
One query for vx why and vx mcp: the CLI defaulted to it while the
tool demanded a run id an agent had to fetch first (2026-09-16).
export function latestRunId(db: Database, taskId: string): string | nullLayeredCache
Section titled “LayeredCache”class · src/cache/layered-cache.ts
export class LayeredCache implements CacheLayer { readonly hasRemote constructor( readonly local: Cache, private readonly remote: RemoteCacheLayer, private readonly options: LayeredCacheOptions = {}, ) key(input: CacheKeyInput): Promise<string> async prefetch(hash: string, ctx?: CacheGetContext): Promise<boolean> async remoteHasMany(hashes: readonly string[]): Promise<Set<string> | null> markRemoteAbsent(hashes: Iterable<string>): void async get(hash: string, ctx?: CacheGetContext): Promise<CacheEntry | null> async has(hash: string): Promise<'local' | 'remote' | null> outputsPath(hash: string): string hashFile(filePath: string): Promise<string> async restoreOutputs(hash: string, projectDir: string, workspaceRoot?: string): Promise<void> async save(args: SaveArgs): Promise<void> async drainUploads(): Promise<void> async ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void> loadOutputFilesBatch(hashes: readonly string[]): Map<string, OutputFileRow[]> async isOutputsCurrent(projectDir: string, expected: readonly OutputFileRow[]): Promise<boolean> recordOutputDirs( hash: string, projectDir: string, prefixes: readonly string[], holds?: (files: readonly string[]) => boolean, ): Promise<void> recordOutputStamps(hash: string, projectDir: string, workspaceRoot: string): void loadOutputDirsBatch(hashes: readonly string[]): Map<string, OutputDirRow[]> outputDirsCurrent(projectDir: string, rows: readonly OutputDirRow[]): Promise<boolean> recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void stats(opts?: CacheStatsOptions): CacheStats prune(options: PruneOptions): Promise<PruneResult> close(): void}listProjectMetas
Section titled “listProjectMetas”function · src/workspace/workspace.ts
export async function listProjects(workspace: Workspace): Promise<ProjectMeta[]>loadProjectConfig
Section titled “loadProjectConfig”function · src/workspace/project-loader.ts
export async function loadProjectConfig( configPath: string, opts?: LoadProjectConfigOptions,): Promise<ProjectConfig>loadResolvedProjects
Section titled “loadResolvedProjects”function · src/orchestrator/projects.ts
The run path’s view of a workspace’s projects for a reader — vx show,
the MCP server, an embedder: discovery, the plugin config and
project stages, and the local cache opened only to serve cached
evaluations, so a pure config costs a stat, not an evaluation. scope
is every project or a list of names; no closure, no lock (a reader
reads live, as a default run does). Plugin warnings go to warn.
export async function loadResolvedProjects( workspaceRoot: string, opts: { scope?: 'all' | readonly string[]; warn?: (message: string) => void } = {},): Promise<Map<string, ProjectEntry>>loadWorkspace
Section titled “loadWorkspace”function · src/workspace/workspace.ts
Read the workspace’s package-glob list, supporting all common package managers:
pnpm-workspace.yaml(pnpm)package.jsonworkspacesarray (npm / yarn / bun)package.jsonworkspaces.packagesarray (yarn legacy)
If a package.json exists with no workspaces field, the root
itself is treated as a single-project workspace.
export async function loadWorkspace(root: string, reads?: LoadReads): Promise<Workspace>LocalHistoryProvider
Section titled “LocalHistoryProvider”class · src/orchestrator/history.ts
Reads from the orchestrator’s local SQLite cache.db.
export class LocalHistoryProvider implements HistoryProvider { constructor( private readonly db: Database, private readonly recent: number = DEFAULT_RECENT, ) {} async loadFor(taskIds: readonly string[]): Promise<HistoryTable>}lockfileClaim
Section titled “lockfileClaim”function · src/orchestrator/lockfile-claim.ts
export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooksLockfileClaimHooks
Section titled “LockfileClaimHooks”type · src/orchestrator/lockfile-claim.ts
The two hooks a lockfile plugin spreads into definePlugin.
export interface LockfileClaimHooks { readonly fingerprint: FingerprintClaim key(task: TaskNode, ctx: KeyHookContext): Promise<Readonly<Record<string, string>> | undefined>}LockfileClaimOptions
Section titled “LockfileClaimOptions”type · src/orchestrator/lockfile-claim.ts
export interface LockfileClaimOptions { readonly file: string readonly digest: (text: string, files: ReadonlyMap<string, string>) => ReadonlyMap<string, string> readonly extraFiles?: (text: string) => readonly string[] readonly version: number readonly scope?: 'project' | 'workspace' readonly part?: string}LOG_WIRE_VERSION
Section titled “LOG_WIRE_VERSION”const · src/orchestrator/task-log-buffer.ts
Version of the drained-bundle shape below. It is the canonical drained logs format, not one transport’s: every sink ships the same object.
export const LOG_WIRE_VERSION = 1Logger
Section titled “Logger”type · src/orchestrator/logger.ts
export interface Logger { status(line: string): void taskStdout(node: TaskNode, chunk: string): void taskStderr(node: TaskNode, chunk: string): void taskComplete(node: TaskNode, outcome: TaskOutcome): void runStart?(info: { total: number concurrency?: number requestedCount?: number context?: RunContext startedAtMs?: number }): void taskStart?(node: TaskNode): void runEnd?(): void}machineMemoryBytes
Section titled “machineMemoryBytes”function · src/util/cgroup.ts
The machine’s total memory, capped by its cgroup limit.
export function machineMemoryBytes(probe: CgroupProbe = {}): numbermachineParallelism
Section titled “machineParallelism”function · src/util/cgroup.ts
The cores this process may run on, capped by its cgroup CPU quota, rounded up (a 1.5-core quota is two workers, not one) and never below one. The default worker count.
export function machineParallelism(probe: CgroupProbe = {}): numberMigrationFormat
Section titled “MigrationFormat”type · src/workspace/migration.ts
export type MigrationFormat = 'ts' | 'mjs'MigrationPlan
Section titled “MigrationPlan”type · src/workspace/migration.ts
export interface MigrationPlan { headerNotes: string[] projects: GeneratedProject[] extraFiles: { relPath: string; contents: string }[] notes: string[]}normalizeGlob
Section titled “normalizeGlob”function · src/util/paths.ts
The spellings a reader, Turbo and .gitignore all accept but a matcher
fed the raw string turns into NOTHING — and a task keyed on nothing
replays old outputs as a green hit (2026-09-10, probed one by one):
a leading ./, an inner /./ segment, a doubled //, and a trailing
/ on a pattern (src/*\/ means the trees under src, so it becomes
src/*\/**; a trailing slash on a LITERAL is asTrees’ job). Applied
after an optional !; a bare . is the empty entry the schema refuses.
export function normalizeGlob(glob: string): stringOutcomeView
Section titled “OutcomeView”type · src/orchestrator/events.ts
Serializable projection of a TaskOutcome (no node ref, ns as strings).
export interface OutcomeView { taskId: string status: TaskOutcome['status'] exitCode: number durationMs: number isGroup?: boolean noCache?: boolean storedDurationMs?: number storedCpuMs?: number storedPeakRssBytes?: number hash?: string cpuMs?: number peakRssBytes?: number timedOut?: true notReady?: 'timeout' | 'exited' | 'spawn' blockedBy?: string admissionHeldMs?: number restored?: boolean sandboxViolations?: number sandboxViolationLines?: string[] wallclockStartNs?: string wallclockEndNs?: string}outputsOverlap
Section titled “outputsOverlap”function · src/graph/task-graph.ts
True only when two output globs PROVABLY select an overlapping set.
Deliberately conservative, because the caller REFUSES the run: a false positive breaks a build that works today, which is worse than the defect being caught. So the three cases are exactly the ones that can be decided without a general glob-intersection algorithm:
both literal — equal paths
literal vs glob — ask the glob whether it matches the literal (exact)
both globs — identical strings, or one a whole subtree P/**
and the other’s literal prefix P or under it: every
path the second matches is under P, so the first
covers it (item 941). Anything else is undecided
here and deliberately allowed through
The rejected alternative was comparing each glob’s static prefix. It is
cheaper and catches more, but it is UNSOUND for a refusal — measured:
dist/vx-* and dist/other.txt share the prefix dist while matching
disjoint sets, so a prefix check refuses a legitimate config. (vx’s own
build.bun.* tasks escape only because they declare distinct literals.)
All three cases compare SPELLINGS, so each side is run through
asTrees first — the same rule the resolver and cleanOutputs read,
and cleanOutputs is what actually does the deleting. That folds two
things this check used to miss, both of them the data loss it exists to
prevent:
- the SPELLING:
./dist/**anddist/**are one tree to every matcher in vx, andBun.Glob('dist/**')does not match the literal./dist/app.jseither (item 441, probed one spelling at a time); - the literal DIRECTORY:
outputs: ['dist']means everything underdist—asTreescompiles it todist+dist/**— while this compared it todist/app.jsas two unequal literals. Measured end to end: the task declaringdistwiped the other’sdist/app.jsand the run reported success (item 442).
Neither is a widening. Both read the declaration the way the code that deletes reads it, which is the only reading that decides the hazard.
Exported through the façade because @vzn/vx-migrate asks the same
question at MIGRATION time — it uncaches the losers so the generated
config loads — and it used to ask it with a copy of this function. The
copy did not get items 441 and 442, so it reported clean on configs
core then refused, including outputs: ['dist'] against
dist/app.js, which is the commonest turbo.json shape there is (item
445). One rule, one place: the copy is gone.
export function outputsOverlap(rawA: string, rawB: string): booleanOutputView
Section titled “OutputView”type · src/orchestrator/logger.ts
The default logger’s per-task output policy. Resolved once per run
from (in priority order) the explicit --output-logs override, a
truthy CI env, and the CLI-detected flow:
full — frames for executed work, one-liners for quiet hits.
Today’s CI behavior; also the programmatic default.
errors-only — only failed tasks print.
none — no per-task output at all.
hash-only — one line per task: outcome word, task id, cache key.
No frames, no log replay (Turbo --output-logs hash-only parity); the end-of-run summary still
renders. The line is the run’s audit trail: which key
each task resolved to, without any build output.
focused — requested nodes stream raw output live (running the
task should feel like running the command directly);
dependency-pulled nodes are silent unless they fail.
broad — news only: one success line per executed task,
full frames for failures, silence for cache hits.
gha (on GitHub Actions, any mode): task output is fenced from
workflow commands. In full mode each task’s block is also wrapped in
::group:: / ::endgroup:: so tasks collapse in the log viewer —
except failed tasks, which stay pre-expanded and emit an ::error
annotation instead.
ci: a truthy CI env was detected. Suppresses the dynamic status
line even if stdout happens to be a TTY.
export interface OutputView { mode: 'full' | 'errors-only' | 'none' | 'focused' | 'broad' | 'hash-only' gha?: boolean ci?: boolean}PERSISTENT_TODO
Section titled “PERSISTENT_TODO”const · src/workspace/migration.ts
The one wording every mapper emits for a task it made persistent.
export const PERSISTENT_TODO = 'persistent task — set persistent.readyWhen (regex matched against output) so ' + 'dependents unblock on readiness, and consider exec.timeout to bound the wait'PlannedTask
Section titled “PlannedTask”type · src/orchestrator/plan.ts
export interface PlannedTask { node: TaskNode hash: string cacheStatus: CacheStatus deps: readonly string[] p50Ms?: number executor?: string download?: 'deferred'}planRun
Section titled “planRun”function · src/orchestrator/run.ts
Planning mode. Same setup as run() — workspace discovery, config
load, package graph, task graph — but stops short of execution.
Returns a RunPlan predicting the cache hit/miss outcome of every
task. Used by --dry-run and --graph.
Side-effects are limited to opening + closing the local Cache handle
(and running cache.inputs.runtime probe commands, which key
derivation requires). Cache probing is the byte-free cache.has()
existence check — no artifact download, no ingest, no accessed_at bump.
export async function planRun(options: RunOptions): Promise<RunPlan>PLUGIN_HOOKS
Section titled “PLUGIN_HOOKS”const · src/config.ts
Every hook a plugin may fill, in pipeline order — THE list. The loader’s
“must be a function” and “at least one of” checks, the host’s stage gate
and vx info’s seam column all read it, so a stage added here is a stage
everywhere: admit reached vx info a day late (2026-09-12) because
that column kept its own copy, and the loader kept a third. The type
pin below refuses a list that drifts from Plugin’s keys either way.
export const PLUGIN_HOOKS = [ 'config', 'project', 'graph', 'key', 'fingerprint', 'schedule', 'admit', 'executor', 'cache', 'telemetry', 'setup', 'commands', 'teardown',] as constPluginCommand
Section titled “PluginCommand”type · src/orchestrator/plugin.ts
One CLI verb contributed by a plugin.
export interface PluginCommand { readonly description: string run(argv: readonly string[], ctx: CommandContext): number | Promise<number>}PluginHook
Section titled “PluginHook”type · src/config.ts
export type PluginHook = (typeof PLUGIN_HOOKS)[number]PluginHooks
Section titled “PluginHooks”type · src/orchestrator/plugin.ts
What a plugin author writes: every hook, and no name.
export type PluginHooks = Omit<VxPlugin, 'name'>PluginOrigin
Section titled “PluginOrigin”type · src/orchestrator/plugin.ts
Where a plugin is defined — import.meta of its module. dir is Bun’s
field; url is the standard one, for a module evaluated elsewhere.
export interface PluginOrigin { readonly dir?: string readonly url?: string}PluginSetupContext
Section titled “PluginSetupContext”type · src/orchestrator/plugin.ts
export interface PluginSetupContext extends BaseContext {}PreparedRun
Section titled “PreparedRun”type · src/orchestrator/prepare.ts
export interface PreparedRun { workspaceRoot: string workspaceConfig: WorkspaceConfig | null plugins: readonly VxPlugin[] cacheDir: string cache: CacheLayer localCache: Cache hasRemoteLayer: boolean priorities: ReadonlyMap<string, number> nodes: Map<string, TaskNode> unresolvedTasks: readonly string[] projects: ReadonlyMap<string, ProjectEntry> anyProjectConfig: boolean workspaceFingerprint: string fingerprintWatch: FingerprintWatch nestedDirsByProject: Map<string, string[]> gitFilesCache: GitFilesCache workspaceProjectCount: number hashCache: HashCache empty: null | 'no-tasks-declared' | 'none-affected' | 'empty-graph'}prepareRun
Section titled “prepareRun”function · src/orchestrator/prepare.ts
Build the prepared-run context: workspace discovery, project-config
load, package + task graph, cache handle (local, optionally wrapped
in a remote layer). Caller owns cache.close().
Returns even when nothing can run — the empty field tells the
caller why. We never throw on “no tasks”; behavior on that case is
caller-specific (run logs + returns NOT-ok; planRun returns an
empty plan).
export async function prepareRun(options: RunOptions, log: Logger): Promise<PreparedRun>ProjectConfig
Section titled “ProjectConfig”type · src/config.ts
export interface ProjectConfig { tasks?: Record<string, TaskConfig>}ProjectEntry
Section titled “ProjectEntry”type · src/workspace/workspace.ts
A discovered project joined with its loaded vx config.
export interface ProjectEntry { name: string dir: string config: ProjectConfig}ProjectHookContext
Section titled “ProjectHookContext”type · src/orchestrator/plugin.ts
export interface ProjectHookContext extends BaseContext { readonly name: string readonly dir: string readonly packageJson: Readonly<Record<string, unknown>> readonly projects: readonly ProjectMeta[]}ProjectMeta
Section titled “ProjectMeta”type · src/workspace/workspace.ts
export interface ProjectMeta { name: string dir: string packageJson: PackageJson configPath: string | null}quoteTsLiteral
Section titled “quoteTsLiteral”function · src/workspace/migration.ts
Escape an arbitrary string into a single-quoted TS literal. Escapes
backslash + quote AND raw newlines/CR — a value with an embedded newline
(legal JSON, e.g. a script "echo a\necho b", or a glob with a ') would
otherwise splice into a single-quoted literal as an unterminated / malformed
string that fails to load (generated files must round-trip through the
loader).
export function quoteTsLiteral(s: string): stringRawExpr
Section titled “RawExpr”type · src/workspace/migration.ts
Verbatim TS expression spliced into a generated array (preset spreads).
export interface RawExpr { readonly raw: string}reachDigests
Section titled “reachDigests”function · src/orchestrator/lockfile-claim.ts
One digest per node over everything the node reaches, Merkle-style: a change anywhere in a node’s reach moves its digest, a change elsewhere does not. Lockfiles carry dependency cycles, so the unit is the strongly connected component: Tarjan’s walk (iterative — a dependency chain can be thousands deep) emits components children-first, and each folds its members (sorted), each as its material and the materials its edges land on, and its child components’ digests (sorted). O(nodes + edges): 1000 importers over 3000 packages digest in ~20 ms where one traversal per importer took 400.
export function reachDigests(g: ReachGraph): string[]ReachGraph
Section titled “ReachGraph”type · src/orchestrator/lockfile-claim.ts
A dependency graph: one material string per node and its out-edges by index.
export interface ReachGraph { readonly material: readonly string[] readonly edges: ReadonlyArray<readonly number[]>}RemoteCacheLayer
Section titled “RemoteCacheLayer”type · src/cache/layered-cache.ts
What a remote cache layer must provide — THE plugin seam for remote
caching. Core ships no wire client; a plugin’s cache capability (or an
embedder via RunOptions.remoteCache) supplies an implementation speaking
whatever protocol it wants, and LayeredCache owns everything else:
policy gating, in-flight dedup, remote provenance, and the never-fail
contract (implementations THROW on failure; LayeredCache degrades every
throw to a cache miss via onRemoteError). The artifact bytes are the
local <hash>.tar.zst verbatim. The wires live in plugin packages
(@vzn/vx-migrate’s turboCache() and nxCache(), @vzn/vx-reapi); see
docs/modules/layered-cache.md.
Core awaits every call and bounds none: a get that never settles
holds its task and a put the run’s upload drain, so each request
carries the layer’s own deadline (every first-party layer has one; the
plugin guide’s example shows it).
export interface RemoteCacheLayer { readonly endpoint?: string has(hash: string): Promise<boolean> hasMany?(hashes: readonly string[]): Promise<Set<string> | null> get(hash: string): Promise<{ body: Blob | Response; durationMs: number | undefined } | null> put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void>}ResolvedSandboxConfig
Section titled “ResolvedSandboxConfig”type · src/exec/sandbox-runtime.ts
Sandbox config with all path fields resolved to absolute paths.
Produced by resolveSandboxConfig. The shape mirrors SandboxConfig
but every string in a path list is guaranteed absolute.
export interface ResolvedSandboxConfig { allowRead: readonly string[] allowWrite: readonly string[] network?: true | readonly string[] denyNetwork?: readonly string[] systemInfo?: readonly string[] unixSockets?: true | readonly string[] localBinding?: boolean | readonly number[] machLookup?: readonly string[] pty?: boolean gitConfig?: boolean weakerWhenNested?: boolean weakerNetworkIsolation?: boolean wallsReached?: { read: readonly string[]; write: readonly string[] } ignore?: { read?: readonly string[] write?: readonly string[] systemInfo?: readonly string[] network?: readonly string[] }}function · src/orchestrator/run.ts
export async function run(options: RunOptions): Promise<RunSummary>RunContextRecord
Section titled “RunContextRecord”type · src/orchestrator/telemetry.ts
Identifies which run a record belongs to + its captured context. Maps cleanly onto OTel CI/CD + VCS resource attributes.
export interface RunContextRecord { runId: string vxVersion: string command: string requestedTasks: readonly string[] cachePolicy: string concurrency: number flow: 'focused' | 'broad' | null workspaceId: string workspaceName: string commitSha: string | null branch: string | null defaultBranch: string | null dirty: boolean | null ci: boolean ciProvider: string | null host: string | null os: string arch: string tags: Readonly<Record<string, string>>}RunOptions
Section titled “RunOptions”type · src/orchestrator/options.ts
export interface RunOptions { cwd: string tasks: readonly string[] projects?: string[] selectedByDiff?: boolean staged?: ReadonlyMap<string, ProjectEntry> discovered?: { root: string; projects: ProjectMeta[] } concurrency?: number cacheDir?: string cache?: CachePolicy remoteRequested?: boolean frozen?: boolean outputLogs?: 'full' | 'errors-only' | 'none' | 'hash-only' download?: 'all' | 'toplevel' | 'none' flow?: 'focused' | 'broad' retries?: number timeout?: number continueMode?: ContinueMode excludeDependencies?: 'all' | readonly string[] forwardArgs?: readonly string[] summarize?: string profile?: string handleSignals?: boolean signal?: AbortSignal holdPersistent?: boolean log?: Logger bus?: EventBus inflight?: Map<string, Promise<void>> tags?: Record<string, string> telemetrySinks?: readonly TelemetrySink[] command?: string remoteCache?: RemoteCacheLayer artifactCeiling?: number}RunPlan
Section titled “RunPlan”type · src/orchestrator/plan.ts
export interface RunPlan { tasks: PlannedTask[] predicted?: PlanPrediction unresolvedTasks?: readonly string[] unresolvedHint?: string downloadDowngrades?: ReadonlyArray<{ taskId: string; reason: string }>}RunRecord
Section titled “RunRecord”type · src/cache/layer.ts
export interface RunRecord { hash?: string project: string task: string status: 'success' | 'failed' | 'cache-hit' | 'cache-hit-remote' | 'skipped' exitCode: number durationMs: number forwardArgs?: readonly string[] startedAt: number endedAt: number runId?: string cpuMs?: number peakRssBytes?: number wallclockStartNs?: bigint wallclockEndNs?: bigint cacheHit?: boolean attempts?: number cached?: boolean blockedBy?: string timedOut?: true sandboxViolations?: number notReady?: 'timeout' | 'exited' | 'spawn'}RunResult
Section titled “RunResult”type · src/orchestrator/run-report.ts
One finished run, reduced to what a report needs. Used to live in
protocol.ts as the return type of the whole-run backend seam; that
seam is gone (a run always executes in-process — see
docs/modules/executor.md), so the shape lives with its only consumer.
export interface RunResult { ok: boolean outcomes: OutcomeView[]}RunSummary
Section titled “RunSummary”type · src/orchestrator/options.ts
export interface RunSummary { ok: boolean outcomes: TaskOutcome[] persistent?: HeldPersistent}RunSummaryRecord
Section titled “RunSummaryRecord”type · src/orchestrator/telemetry.ts
A per-run SUMMARY record — the denormalized invocation header plus the per-task outcome list, emitted once at run:end. An ingesting store can persist a whole run in one write without replaying the stream. The manual-API exporter + a service’s ingest endpoint primarily speak this shape.
export interface RunSummaryRecord { v: number run: RunContextRecord startedAt: number endedAt: number totalDurationMs: number taskCount: number failedCount: number abortedCount: number hitCount: number hitLocalCount: number hitRemoteCount: number exitOk: boolean tasks: readonly TaskTelemetry[]}SandboxConfig
Section titled “SandboxConfig”type · src/config.ts
What a sandboxed task may do, declared as CAPABILITIES.
One shape for the whole policy. You say what the task is allowed to touch; vx decides how each capability is realised — some become sandbox runtime config, some become rules in the OS policy, and one the platform cannot express is an error rather than a silent no-op. None of that reaches the config.
The baseline (sandbox: {}) grants almost nothing: the task reads
nothing, writes nothing and reaches no network — NOT EVEN ITS OWN
PROJECT DIRECTORY, which is why allow: { read: ['.'] } opens almost
every real block. The one grant vx makes for you is dependencies:
node_modules, and through it the real path of every workspace
package linked there.
cache grants no access in either direction: cache.inputs says what
INVALIDATES a task, allow says what it may TOUCH, and deriving one
from the other coupled them both ways — a declaration added for
caching silently widened the sandbox, and a path the task needed had
to be laundered through the cache key to get it (owner, 2026-09-05).
A declared cache.outputs is NOT a write grant; the request
sandbox-request.ts builds carries no write of its own and binds
allow.write alone. This comment claimed the opposite — here and on
both grant fields — while the file beside it and schema.md both said
the truth, and no test read the bare baseline it described (item 443).
That case has a test of its own now.
Enforcement anchors at the WORKSPACE ROOT, so a task cannot leave its
project whatever it declares here. The full account, including what a
write grant costs on Linux, is in docs/schema.md under
exec.sandbox.
Paths are project-relative, absolute, or ~-expanded, and may be
globs: macOS matches the pattern in the policy, Linux expands it when
the task starts (a mount cannot hold a pattern), so a file created
later is not covered there — grant its directory.
export interface SandboxConfig { allow?: SandboxGrants deny?: SandboxDenials ignore?: SandboxIgnore weakerWhenNested?: boolean weakerNetworkIsolation?: boolean}SandboxDenials
Section titled “SandboxDenials”type · src/config.ts
export interface SandboxDenials { network?: string[]}SandboxGrants
Section titled “SandboxGrants”type · src/config.ts
export interface SandboxGrants { read?: string[] write?: string[] network?: true | string[] systemInfo?: string[] unixSockets?: true | string[] localBinding?: boolean | number[] machLookup?: string[] pty?: boolean gitConfig?: boolean}ScheduleHookContext
Section titled “ScheduleHookContext”type · src/orchestrator/plugin.ts
export interface ScheduleHookContext extends BaseContext { readonly localCache: Cache}splitTaskId
Section titled “splitTaskId”function · src/util/task-id.ts
The inverse of taskId (graph/task-graph.ts). Splits on the FIRST
#, so a task name that itself contains one round-trips: taskId('a', 'b#c') → 'a#b#c' → ['a', 'b#c'].
This exists because it kept being written by hand as id.split('#', 2),
which is NOT the inverse — it discards everything after the second segment,
so 'a#b#c' reads back as task 'b'. Six call sites had that form while
parseDependencySpec (the surface that decides what actually runs) has
always split on the first #, so the query layer and the graph disagreed
about the identity of the same task: a lookup either found nothing or, worse,
answered with a different task’s history.
A #-containing task name is legal and pinned elsewhere in the suite, so
this is reachable rather than theoretical. The cache’s run history read
the same rule from a private copy until item 646; one rule, one place.
export function splitTaskId(id: string): [project: string, task: string]TaskConfig
Section titled “TaskConfig”type · src/config.ts
export interface TaskConfig { description?: string exec?: ExecConfig dependsOn?: readonly string[] cache?: CacheConfig}TaskExecutor
Section titled “TaskExecutor”type · src/exec/executor.ts
export interface TaskExecutor { readonly name: string readonly remote?: boolean readonly capacity?: number accepts?(task: TaskPlacement): boolean demand?(remaining: ReadonlySet<string>): void execute(req: ExecuteRequest): Promise<ExecuteResult>}TaskHistory
Section titled “TaskHistory”type · src/orchestrator/history.ts
Per (project#task) — last RECENT runs collapsed into a summary.
export interface TaskHistory { runs: number p50DurationMs: number | undefined p99DurationMs: number | undefined successRate: number hitRate: number failureMode: FailureMode maxPeakRssBytes?: number maxCpuParallelism?: number}TaskLogBuffer
Section titled “TaskLogBuffer”class · src/orchestrator/task-log-buffer.ts
Bounded per-run capture. append keeps a chunk LIST + running char count per
task, evicting whole chunks from the head past TASK_LOG_TAIL_CHARS — no
string concatenation until drain, so a cache-hit replay (one big chunk) is
one array push, zero copies. finish decides retention; drain emits the
bundle, failures first.
export class TaskLogBuffer { append(taskId: string, chunk: string): void finish(taskId: string, status: TaskStatus, cacheSource: CacheSource, hash?: string): void takeEntry(taskId: string): TaskLogEntry | undefined drain(runId: string, workspaceId: string): TaskLogBundle size(): number budgetUsed(): number}TaskLogBundle
Section titled “TaskLogBundle”type · src/orchestrator/task-log-buffer.ts
export interface TaskLogBundle { v: typeof LOG_WIRE_VERSION runId: string workspaceId: string tasks: TaskLogEntry[]}TaskLogEntry
Section titled “TaskLogEntry”type · src/orchestrator/task-log-buffer.ts
export interface TaskLogEntry { taskId: string hash?: string status: 'success' | 'failed' content: string charsFull: number truncatedHeadChars: number}TaskNode
Section titled “TaskNode”type · src/graph/task-graph.ts
export interface TaskNode { id: string projectName: string projectDir: string taskName: string config: TaskConfig deps: string[] orderOnly?: string[] requested: boolean surfaced?: boolean keyParts?: ReadonlyArray<readonly [name: string, value: string]> addsToOutputsOf?: string[] outputsAddedToBy?: string[] excludedUpstream?: TaskOutcome[]}TaskOutcome
Section titled “TaskOutcome”type · src/graph/scheduler.ts
export interface TaskOutcome { node: TaskNode status: TaskStatus exitCode: number durationMs: number hash?: string storedDurationMs?: number storedCpuMs?: number storedPeakRssBytes?: number admissionHeldMs?: number cpuMs?: number peakRssBytes?: number groupUpstream?: readonly TaskOutcome[] unkeyed?: true blockedBy?: string timedOut?: true notReady?: 'timeout' | 'exited' | 'spawn' where?: string outputs?: 'deferred' wallclockStartNs?: bigint wallclockEndNs?: bigint restored?: boolean attempts?: number sandboxViolations?: number sandboxViolationLines?: string[]}TaskPlacement
Section titled “TaskPlacement”type · src/exec/executor.ts
What an executor sees when a task is PLACED — once per task, before scheduling.
export interface TaskPlacement { readonly taskId: string readonly projectName: string readonly projectDir: string readonly command: string readonly pinnedLocal: boolean readonly cacheable: boolean}TaskStatus
Section titled “TaskStatus”type · src/graph/scheduler.ts
export type TaskStatus = | 'success' | 'cache-hit' | 'cache-hit-remote' | 'failed' | 'skipped' | 'aborted'TaskTelemetry
Section titled “TaskTelemetry”type · src/orchestrator/telemetry.ts
Denormalized per-task analytics — shared by the streaming task.end
record and the per-run summary’s tasks[].
export interface TaskTelemetry { taskId: string project: string task: string status: TaskStatus cacheSource: CacheSource exitCode: number durationMs: number hash?: string cpuMs?: number peakRssBytes?: number where?: string outputs?: 'deferred' attempts?: number blockedBy?: string timedOut?: true sandboxViolations?: number notReady?: 'timeout' | 'exited' | 'spawn' wallclockStartNs?: string wallclockEndNs?: string}TaskView
Section titled “TaskView”type · src/orchestrator/events.ts
Display projection of a TaskNode — exactly the fields renderers read.
export interface TaskView { id: string project: string task: string isGroup: boolean requested: boolean surfaced: boolean persistent: boolean command?: string}TELEMETRY_SCHEMA_VERSION
Section titled “TELEMETRY_SCHEMA_VERSION”const · src/orchestrator/telemetry.ts
Bumped when the record shape changes. Readers MUST check v.
export const TELEMETRY_SCHEMA_VERSION = 3TelemetryContext
Section titled “TelemetryContext”type · src/orchestrator/telemetry.ts
Read-only context a sink is created with. No mutable run handle — the isolation guarantee is structural.
export interface TelemetryContext { readonly workspaceRoot: string readonly cacheDir: string warn(message: string): void}TelemetryRecord
Section titled “TelemetryRecord”type · src/orchestrator/telemetry.ts
A streaming telemetry record — one per lifecycle event. A superset of the
rendering-oriented WireEvent: it carries the run context + the per-task
analytics fields a consumer needs WITHOUT re-deriving from the stream.
task.log records are large and OPT-IN (see TelemetrySink.wants).
export type TelemetryRecord = | { v: number kind: 'run.start' run: RunContextRecord total: number ts: number startedAt: number } | { v: number kind: 'task.start' runId: string taskId: string project: string task: string command?: string ts: number } | { v: number kind: 'task.log' runId: string taskId: string stream: 'stdout' | 'stderr' chunk: string ts: number } | ({ v: number; kind: 'task.end'; runId: string; ts: number } & TaskTelemetry) | { v: number; kind: 'run.end'; runId: string; ts: number }TelemetrySink
Section titled “TelemetrySink”type · src/orchestrator/telemetry.ts
A telemetry consumer. Observe-only: receives records, holds no run handle.
export interface TelemetrySink { readonly name?: string readonly wants?: ReadonlyArray<TelemetryRecord['kind']> onRecord?(record: TelemetryRecord): void onRunSummary?(summary: RunSummaryRecord): void flush?(signal: AbortSignal): Promise<void>}UserError
Section titled “UserError”class · src/util/errors.ts
export class UserError extends Error { constructor(message: string)}VERSION
Section titled “VERSION”const · src/version.ts
export const VERSION: string = pkg.versionVxPlugin
Section titled “VxPlugin”type · src/orchestrator/plugin.ts
A vx plugin. Contributes any subset of the run-level capabilities — where work runs (executor), which cache is used (cache), who observes the run (telemetry). It never changes WHAT a task is (the command string — principle #3), only where and how that command is executed. Registered explicitly in vx.workspace.ts via defineWorkspace({ plugins: […] }). No auto-discovery.
The old observe-only Plugin ({ name, setup(ctx) }) is a subset of
this shape: a plugin with only setup installs and runs exactly as
before via installPlugins. The capabilities are consulted by
plugin-host.ts; core’s own executor and cache are plugins too
(src/plugins/), declared by the workspace — there is no fallback
outside the list.
export interface VxPlugin { readonly name: string config?(workspace: WorkspaceConfig, ctx: WorkspaceHookContext): void | Promise<void> project?(config: ProjectConfig, ctx: ProjectHookContext): void | Promise<void> graph?(nodes: Map<string, TaskNode>, ctx: GraphHookContext): void | Promise<void> key?( task: TaskNode, ctx: KeyHookContext, ): | Readonly<Record<string, string>> | undefined | Promise<Readonly<Record<string, string>> | undefined> readonly fingerprint?: FingerprintClaim schedule?( nodes: ReadonlyMap<string, TaskNode>, ctx: ScheduleHookContext, ): ReadonlyMap<string, number> | undefined | Promise<ReadonlyMap<string, number> | undefined> admit?(task: TaskNode, ctx: AdmitContext): boolean readonly commands?: Readonly<Record<string, PluginCommand>> cache?(ctx: CacheContext): CacheLayer | undefined | Promise<CacheLayer | undefined> executor?(ctx: ExecutorContext): TaskExecutor | undefined | Promise<TaskExecutor | undefined> telemetry?( ctx: TelemetryContext, ): | TelemetrySink | TelemetrySink[] | undefined | Promise<TelemetrySink | TelemetrySink[] | undefined> setup?(ctx: PluginSetupContext): void | Promise<void> teardown?(): void | Promise<void>}whyDidThisRerunQuery
Section titled “whyDidThisRerunQuery”function · src/orchestrator/metrics.ts
export function whyDidThisRerun(db: Database, runId: string, taskId: string): WhyDidThisRerunWorkspaceConfig
Section titled “WorkspaceConfig”type · src/config.ts
export interface WorkspaceConfig { concurrency?: number cacheDir?: string timeout?: number cacheRetention?: { olderThan?: string; maxSize?: string } plugins?: readonly Plugin[]}WorkspaceHookContext
Section titled “WorkspaceHookContext”type · src/orchestrator/plugin.ts
config runs before the cache dir is known — it may be what the hook changes.
export interface WorkspaceHookContext { readonly workspaceRoot: string warn(message: string): void}WorkspaceIdentity
Section titled “WorkspaceIdentity”type · src/orchestrator/run-context.ts
export interface WorkspaceIdentity { id: string name: string}