src/orchestrator/plugin.ts — the VxPlugin interface + installer
Purpose
Section titled “Purpose”The integration seam. A plugin is definePlugin(import.meta, hooks)
— its name is the name of the package it is defined in, read from the
nearest package.json and stamped where the workspace loader checks,
never a field — with at least one capability; defineWorkspace({ plugins: [...] }) activates it. Core
consults capabilities at fixed points and otherwise ignores plugins —
behavior lives in the plugin package (vite-style), not in core.
Capabilities
Section titled “Capabilities”| Capability | Consulted by | Contract |
|---|---|---|
executor(ctx) | plugin-host.ts | return a TaskExecutor or decline; ALL kept in order, first accepting runs |
config(ws, ctx) | every verb, first | edit the workspace config in place before anything is derived from it (cacheDir too); re-validated after EACH plugin |
project(cfg, ctx) | per loaded config | add/remove/edit a project’s tasks in place; core re-validates after EACH plugin, by name |
graph(nodes, ctx) | after graph build | edit deps/requested in place; the builder’s checks run again (item 981) |
key(task, ctx) | per task, at hash | { name: value } material folded into the key and named in vx why |
fingerprint | claim, static | { files, affected(change, ctx) }: the workspace-fingerprint files this plugin keys per project; one claimant each |
schedule(nodes, ctx) | before scheduling | task id → weight, merged over the structural baseline; later plugin wins per task |
admit(task, ctx) | every local dispatch | false holds a ready task beside ctx.running until something finishes (with nothing running, it is overridden, by name); sync, cheap; a throw or a Promise admits from then on |
commands | unknown CLI verb | { verb: { description, run(argv, ctx) } }; a core verb’s name is refused; vx help |
cache(ctx) | run setup | return a CacheLayer or decline; ALL kept in order and chained (see chained-cache.md) |
telemetry(ctx) | telemetry-host.ts | return sink(s) or decline |
setup(ctx) | installPlugins | validate config; throw UserError |
teardown() | end-of-run | flush/close; crash-isolated, 3s-bounded |
Invariants
Section titled “Invariants”- Decline-fast: every capability must return
undefinedcheaply when unconfigured — a plain run with declared-but-unconfigured plugins is zero-overhead (measured ~116ms unchanged). - A throw in
setup, a stage (config,project,graph,key,schedule) or anexecutor/cachefactory fails the run in one line naming the plugin and the hook: what a plugin shapes is load-bearing. The observers are isolated (observability never breaks a run): atelemetryfactory or sink, actx.onhandler andteardownare warned and switched off, and a throwingadmit(or one that answers a Promise) admits from then on. An async hook’s rejection counts as its throw.ctx.onwith a hook name it does not know fails the load (H-16). teardown()and every telemetry sink’sflush()ARE invoked at end-of-run, each under try/catch and a time bound — plugins may rely on them to drain buffers. A run a SIGINT/SIGTERM/SIGHUP stops is no exception (item 849), nor is one that never started its schedule (a refused setup, a factory that threw, an unresolved name) or a plan (item 1021); a second signal, or akill -9, is. A plugin whose ownsetupthrew is not torn down. (The oldereventSinkseam is gone since pipeline v2;setup(ctx)on the bus andtelemetryare the two observe paths.)- No defaults, one floor. Core applies no plugin on its own; its local executor and local cache are appended at the tail of every list and chain (see plugin-host.md), so a workspace that declares nothing runs and caches here.