@vzn/vx 0.0.636 → 0.0.638

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +39 -11
  2. package/docs/caching.md +246 -213
  3. package/docs/cli.md +791 -705
  4. package/docs/execution.md +68 -76
  5. package/docs/features.md +47 -42
  6. package/docs/patterns.md +26 -25
  7. package/docs/schema.md +392 -205
  8. package/docs/security.md +76 -39
  9. package/launcher.sh +43 -0
  10. package/package.json +5 -5
  11. package/schemas/cache.json +41 -19
  12. package/schemas/info.json +23 -11
  13. package/schemas/last.json +48 -2
  14. package/schemas/lock.json +1 -1
  15. package/schemas/plan.json +4 -4
  16. package/schemas/show.json +5 -3
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +28 -13
  19. package/src/bin.ts +30 -9
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +124 -53
  22. package/src/cache/chained-cache.ts +4 -0
  23. package/src/cache/git-inputs.ts +277 -63
  24. package/src/cache/index.ts +15 -1
  25. package/src/cache/inputs.ts +102 -36
  26. package/src/cache/key-fold.ts +8 -7
  27. package/src/cache/layer.ts +15 -18
  28. package/src/cache/layered-cache.ts +20 -102
  29. package/src/cache/plugin-cache.ts +248 -0
  30. package/src/cache/policy.ts +32 -27
  31. package/src/cache/run-history.ts +104 -14
  32. package/src/cache/run-store.ts +77 -0
  33. package/src/cache/schema.ts +58 -31
  34. package/src/cache/scoped-cache.ts +237 -0
  35. package/src/cli/cache.ts +173 -34
  36. package/src/cli/completions.ts +11 -7
  37. package/src/cli/core-alias.ts +1 -1
  38. package/src/cli/foreign-flags.ts +13 -12
  39. package/src/cli/help.ts +110 -70
  40. package/src/cli/index.ts +67 -24
  41. package/src/cli/info.ts +43 -27
  42. package/src/cli/init.ts +115 -407
  43. package/src/cli/last.ts +157 -9
  44. package/src/cli/lock.ts +356 -122
  45. package/src/cli/plan-format.ts +7 -5
  46. package/src/cli/plugin-commands.ts +81 -14
  47. package/src/cli/plugin-templates.ts +11 -11
  48. package/src/cli/run.ts +116 -160
  49. package/src/cli/select.ts +50 -47
  50. package/src/cli/show.ts +83 -59
  51. package/src/cli/task-verb.ts +6 -13
  52. package/src/cli/upgrade.ts +415 -149
  53. package/src/cli/watch-cycle.ts +9 -2
  54. package/src/cli/watch-filter.ts +39 -61
  55. package/src/cli/watch-fs.ts +29 -16
  56. package/src/cli/watch-judge.ts +18 -4
  57. package/src/cli/watch-set.ts +16 -42
  58. package/src/cli/watch.ts +227 -230
  59. package/src/cli/why.ts +38 -18
  60. package/src/cli/workspace-config.ts +33 -70
  61. package/src/config.ts +169 -70
  62. package/src/exec/env.ts +1 -31
  63. package/src/exec/executor.ts +1 -5
  64. package/src/exec/kill-tree.ts +2 -1
  65. package/src/exec/sandbox-runtime.ts +12 -1
  66. package/src/exec/sandbox-violations.ts +9 -1
  67. package/src/graph/dependency-spec.ts +1 -1
  68. package/src/graph/index.ts +1 -0
  69. package/src/graph/priorities.ts +1 -1
  70. package/src/graph/scheduler.ts +27 -7
  71. package/src/graph/task-graph.ts +76 -24
  72. package/src/handoff.ts +169 -0
  73. package/src/index.ts +35 -58
  74. package/src/orchestrator/affected-tasks.ts +15 -8
  75. package/src/orchestrator/command-windows.ts +44 -0
  76. package/src/orchestrator/deferred-outputs.ts +12 -10
  77. package/src/orchestrator/doctor.ts +98 -110
  78. package/src/orchestrator/download-policy.ts +3 -3
  79. package/src/orchestrator/events.ts +19 -3
  80. package/src/orchestrator/excluded-keys.ts +0 -2
  81. package/src/orchestrator/execute-task.ts +147 -69
  82. package/src/orchestrator/failure-mode.ts +19 -10
  83. package/src/orchestrator/fingerprint-watch.ts +18 -15
  84. package/src/orchestrator/forecast.ts +10 -2
  85. package/src/orchestrator/framed-output.ts +9 -5
  86. package/src/orchestrator/graph-stage.ts +5 -5
  87. package/src/orchestrator/history.ts +126 -30
  88. package/src/orchestrator/hit-restore.ts +15 -3
  89. package/src/orchestrator/index.ts +20 -5
  90. package/src/orchestrator/local-shortcircuit.ts +2 -1
  91. package/src/orchestrator/lockfile-claim.ts +79 -20
  92. package/src/orchestrator/logger.ts +80 -41
  93. package/src/orchestrator/metrics.ts +78 -9
  94. package/src/orchestrator/miss-save.ts +3 -0
  95. package/src/orchestrator/options.ts +42 -29
  96. package/src/orchestrator/output-host.ts +72 -0
  97. package/src/orchestrator/plan.ts +6 -8
  98. package/src/orchestrator/plugin-host.ts +100 -66
  99. package/src/orchestrator/plugin.ts +173 -67
  100. package/src/orchestrator/prepare.ts +163 -78
  101. package/src/orchestrator/projects.ts +105 -99
  102. package/src/orchestrator/remote-prefetch.ts +11 -6
  103. package/src/orchestrator/run-context.ts +25 -6
  104. package/src/orchestrator/run-failures.ts +17 -8
  105. package/src/orchestrator/run-lock.ts +1 -1
  106. package/src/orchestrator/run-records.ts +1 -1
  107. package/src/orchestrator/run-report.ts +9 -9
  108. package/src/orchestrator/run-settings.ts +48 -0
  109. package/src/orchestrator/run.ts +244 -119
  110. package/src/orchestrator/sandbox-fix.ts +39 -20
  111. package/src/orchestrator/sandbox-request.ts +84 -13
  112. package/src/orchestrator/signals.ts +1 -1
  113. package/src/orchestrator/stable-keys.ts +13 -11
  114. package/src/orchestrator/status-line.ts +49 -7
  115. package/src/orchestrator/summary.ts +115 -106
  116. package/src/orchestrator/task-hash.ts +12 -83
  117. package/src/orchestrator/telemetry.ts +4 -4
  118. package/src/settings.ts +428 -0
  119. package/src/{orchestrator → util}/colors.ts +5 -0
  120. package/src/util/config-frame.ts +31 -2
  121. package/src/util/errors.ts +33 -1
  122. package/src/util/essential-env.ts +31 -0
  123. package/src/util/hide-env.ts +107 -0
  124. package/src/util/index.ts +16 -3
  125. package/src/util/paths.ts +10 -0
  126. package/src/util/refusal-notice.ts +111 -0
  127. package/src/util/report.ts +31 -0
  128. package/src/util/settle.ts +49 -30
  129. package/src/util/size.ts +23 -4
  130. package/src/util/timing.ts +3 -2
  131. package/src/util/ui.ts +186 -0
  132. package/src/util/ulid.ts +1 -1
  133. package/src/util/verbs.ts +0 -15
  134. package/src/workspace/affected.ts +84 -232
  135. package/src/workspace/config-eval.ts +15 -20
  136. package/src/workspace/config-imports.ts +78 -3
  137. package/src/workspace/config-schema.ts +209 -268
  138. package/src/workspace/dependency-rules.ts +65 -0
  139. package/src/workspace/filter.ts +2 -2
  140. package/src/workspace/fingerprint.ts +11 -121
  141. package/src/workspace/index.ts +20 -27
  142. package/src/workspace/load-reads.ts +4 -4
  143. package/src/workspace/lockfile.ts +96 -140
  144. package/src/workspace/migration.ts +307 -215
  145. package/src/workspace/package-graph.ts +30 -216
  146. package/src/workspace/project-loader.ts +80 -285
  147. package/src/workspace/repo-id.ts +2 -1
  148. package/src/workspace/workspace.ts +363 -831
  149. package/src/cache/config-evals.ts +0 -130
  150. package/src/workspace/config-cache.ts +0 -661
  151. package/src/workspace/migrate-scripts.ts +0 -1121
@@ -11,21 +11,28 @@
11
11
  import { readFileSync } from 'node:fs'
12
12
  import path from 'node:path'
13
13
  import { fileURLToPath } from 'node:url'
14
- import type { Cache, CacheLayer, CachePolicy } from '../cache/index.js'
14
+ import {
15
+ absentPluginCache,
16
+ type Cache,
17
+ type PluginCache,
18
+ type RemoteCacheLayer,
19
+ } from '../cache/index.js'
15
20
  import { PLUGIN_PACKAGE, type ProjectConfig, type WorkspaceConfig } from '../config.js'
16
21
  import type { TaskExecutor } from '../exec/index.js'
17
- import type { TaskNode, TaskOutcome } from '../graph/index.js'
22
+ import type { TaskNode, TaskOutcome, TaskStatus } from '../graph/index.js'
18
23
  import { maskedLine, nearest, UserError } from '../util/index.js'
19
24
  import type { ProjectMeta } from '../workspace/index.js'
20
25
  import type { EventBus, RunStartInfo } from './events.js'
26
+ import type { CiContext } from './run-context.js'
21
27
  import type { TelemetryContext, TelemetrySink } from './telemetry.js'
28
+ import type { Logger } from './logger.js'
22
29
 
23
30
  /**
24
31
  * A vx plugin. Contributes any subset of the run-level capabilities —
25
32
  * where work runs (executor), which cache is used (cache), who
26
33
  * observes the run (telemetry). It never changes WHAT a task is (the
27
34
  * command string — principle #3), only where and how that command is
28
- * executed. Registered explicitly in vx.workspace.ts via
35
+ * executed. Registered explicitly in .vx/workspace.ts via
29
36
  * defineWorkspace({ plugins: [...] }). No auto-discovery.
30
37
  *
31
38
  * Made by `definePlugin(import.meta, hooks)` only: the loader refuses a
@@ -59,24 +66,32 @@ export interface VxPlugin {
59
66
  config?(workspace: WorkspaceConfig, ctx: WorkspaceHookContext): void | Promise<void>
60
67
 
61
68
  /**
62
- * Directories to make projects beyond the package manager's members, as
63
- * `{ dir, name }` (`dir` absolute or relative to the workspace root): a
64
- * runner's root tasks, a project no member glob lists. Runs once per
65
- * discovery, before any project config loads; each plugin sees core's
66
- * projects and the ones earlier plugins named. A named directory keeps
67
- * a `package.json` of the same name, if it has one; a name or directory
68
- * already taken otherwise, or one outside the root, is refused.
69
+ * Projects beyond the directories holding a vx.config, their names, and
70
+ * edges between projects, as `DiscoveredProject`s (each directory
71
+ * absolute or relative to the workspace root): a lockfile's importers,
72
+ * their package names and the dependencies it records; a runner's root
73
+ * tasks. Runs once per discovery, before any project config loads; each
74
+ * plugin sees the locked projects and the ones earlier plugins added,
75
+ * without their edges. A directory that is not a project becomes one
76
+ * (with no config; named by `name`, else its path). A project named by
77
+ * its path takes `name`; one its config names keeps that name, and a
78
+ * different name from an earlier plugin is refused, as is a directory
79
+ * outside the root. Two projects of one name are refused once the stage
80
+ * ends. `deps` and `reach` from every plugin union then; a target that is
81
+ * no project is refused.
69
82
  */
70
- discover?(ctx: DiscoverContext): readonly NamedProject[] | Promise<readonly NamedProject[]>
83
+ discover?(
84
+ ctx: DiscoverContext,
85
+ ): readonly DiscoveredProject[] | Promise<readonly DiscoveredProject[]>
71
86
 
72
87
  /**
73
88
  * One project's validated config, right after it loaded and before the
74
89
  * graph is built. Add, remove or edit tasks. Runs for every loaded project
75
- * on every run — a config's cached evaluation is the user's file, and the
76
- * hook is applied on top of it live. A package with NO config file is
77
- * loaded too, as `{ tasks: {} }`, so a plugin can give tasks to packages
78
- * that never wrote one (`ctx.packageJson` carries their scripts); with no
79
- * `project` plugin such a package is never visited.
90
+ * on every run — a locked config is the user's file, and the hook is
91
+ * applied on top of it live. A project with NO config file (a
92
+ * `discover` plugin added it) is loaded too, as `{ tasks: {} }`, so a
93
+ * plugin can give it tasks; with no `project` plugin such a project is
94
+ * never visited.
80
95
  */
81
96
  project?(config: ProjectConfig, ctx: ProjectHookContext): void | Promise<void>
82
97
 
@@ -105,22 +120,14 @@ export interface VxPlugin {
105
120
  | Promise<Readonly<Record<string, string>> | undefined>
106
121
 
107
122
  /**
108
- * Workspace-root files this plugin keys on its own, taken OUT of the
109
- * workspace fingerprint. Core folds every lockfile it knows into every
110
- * task's key, so one `pnpm install` re-keys the whole workspace. A plugin
111
- * that reads the lockfile and folds each project's own dependency
112
- * closure through `key` claims the file here: core leaves it out of the
113
- * fingerprint every key sees (the config-evaluation cache still keys on
114
- * it — a config may import a dependency), and `--affected` asks
115
- * `affected` which projects a change to it touches instead of selecting
116
- * every project. A file has one claimant.
117
- *
118
- * A root file core does NOT fold is claimed the same way when the
119
- * plugin's stages read it: `turbo()` shapes every task from
120
- * `turbo.json`, so an edit to it re-keys tasks through their resolved
121
- * configs while no project owns the path. Nothing is taken out;
122
- * `--affected` asks `affected` (undefined: every project) and `vx watch`
123
- * re-runs on an edit (item 961). A claim is a bare name at the root.
123
+ * Workspace-root files this plugin keys on: core folds no root file into
124
+ * any key, so a plugin that reads one (a lockfile it folds per project
125
+ * through `key`, `turbo.json` that shapes every task through its
126
+ * `project` stage) claims it here. `--affected` asks `affected` which
127
+ * projects a change to it touches (undefined: every project), `vx watch`
128
+ * re-runs on an edit (item 961), and a task that rewrites one during the
129
+ * run withholds every later save. A claim is a bare name at the root; a
130
+ * file has one claimant.
124
131
  */
125
132
  readonly fingerprint?: FingerprintClaim
126
133
 
@@ -167,14 +174,14 @@ export interface VxPlugin {
167
174
  // --- BEHAVIOR capabilities (change WHAT/HOW work runs — opt-in) -----------
168
175
 
169
176
  /**
170
- * Contribute a cache layer. Returns a CacheLayer wrapping (or replacing)
171
- * the local Cache, or undefined to decline. Consulted ONCE per prepareRun.
172
- * Every contributed layer is kept and chained in declaration order,
173
- * ahead of core's own `.vx/cache` handle at the tail: a lookup walks
174
- * them until one hits, a save reaches each. A layer that WRAPS the local
175
- * handle subsumes that tail, so it is not written twice.
177
+ * Contribute a remote cache: the wire (`has`, `get`, `put`, optional
178
+ * `hasMany`), or undefined to decline. Consulted ONCE per prepareRun.
179
+ * Core layers each one behind its own local cache and owns the rest:
180
+ * the local store is asked first, then each remote in declaration order;
181
+ * the run's policy and scope gate every remote; a remote's throw is a
182
+ * miss and a warning naming the plugin, never a failed run.
176
183
  */
177
- cache?(ctx: CacheContext): CacheLayer | undefined | Promise<CacheLayer | undefined>
184
+ cache?(ctx: CacheContext): RemoteCacheLayer | undefined | Promise<RemoteCacheLayer | undefined>
178
185
 
179
186
  /**
180
187
  * Contribute a task executor — WHERE one task's command runs. Consulted
@@ -206,19 +213,65 @@ export interface VxPlugin {
206
213
  | undefined
207
214
  | Promise<TelemetrySink | TelemetrySink[] | undefined>
208
215
 
216
+ /**
217
+ * Take the terminal for this run: return the `Logger` that renders it
218
+ * (the contract `run({ log })` takes), or `undefined` to leave core's
219
+ * renderer in place. The first plugin, in declaration order, that
220
+ * returns one renders the run; the rest are not asked. Not consulted
221
+ * when an embedder passed its own `log`. A logger that throws hands the
222
+ * rest of the run back to core's renderer with one warning.
223
+ */
224
+ output?(ctx: OutputContext): Logger | undefined | Promise<Logger | undefined>
225
+
209
226
  /**
210
227
  * Optional one-time setup per run (validate the workspace, open a
211
228
  * connection, read a token). Throwing aborts the run with a clean
212
229
  * UserError naming the plugin. It is not a guard over the pipeline: the
213
- * config, project, cache, graph, key and schedule stages have run by the
214
- * time it is called, and a plan (`--dry`, `planRun`) never calls it. It
215
- * precedes the executors, the telemetry sinks, admission and the first
216
- * task (tests/plugin-e2e.test.ts).
230
+ * config, project, cache, graph and key stages have run by the time it
231
+ * is called, and a plan (`--dry`, `planRun`) never calls it. It
232
+ * precedes the schedule stage, the executors, the telemetry sinks,
233
+ * admission and the first task (tests/plugin-e2e.test.ts).
217
234
  */
218
235
  setup?(ctx: PluginSetupContext): void | Promise<void>
219
236
 
220
- /** Optional teardown at end-of-run (flush a sink, close a socket). Errors are logged, never thrown. */
221
- teardown?(): void | Promise<void>
237
+ /**
238
+ * Optional teardown at end-of-run (flush a sink, close a socket). Errors
239
+ * are logged, never thrown. A run that ran to its end passes its
240
+ * `RunReport`; every other exit (a refused setup, a stop) passes none.
241
+ */
242
+ teardown?(run?: RunReport): void | Promise<void>
243
+ }
244
+
245
+ /**
246
+ * What a finished run hands each plugin's `teardown`: its facts and every
247
+ * task, with the names core kept the task's bytes under. Core keeps them
248
+ * only when a remote takes the run's writes (CI by default): a failed
249
+ * task's log and the files its outputs left, an uncached task's log. Read
250
+ * them with `ctx.cache.getRun(name)`. Core keeps no manifest of its own; a
251
+ * plugin that wants one writes it from here (`ctx.cache.put`).
252
+ */
253
+ export interface RunReport {
254
+ /** The `run_id` the run's history rows and telemetry carry. */
255
+ readonly runId: string
256
+ /** `HEAD`'s commit, null outside a git worktree. */
257
+ readonly commit: string | null
258
+ /** What the CI provider says about this run: provider, run URL, pipeline, job, attempt. */
259
+ readonly ciRun: CiContext
260
+ readonly ok: boolean
261
+ /** Every task the run decided, in completion order; group tasks are left out. */
262
+ readonly tasks: readonly RunReportTask[]
263
+ }
264
+
265
+ export interface RunReportTask {
266
+ readonly id: string
267
+ readonly status: TaskStatus
268
+ readonly durationMs: number
269
+ /** The task's cache key, when one was computed. */
270
+ readonly key?: string
271
+ /** Its output (both streams, secrets masked), when core kept it: `ctx.cache.getRun(log)`. */
272
+ readonly log?: string
273
+ /** A tar of the files its declared outputs left, when core kept it: `ctx.cache.getRun(outputs)`. */
274
+ readonly outputs?: string
222
275
  }
223
276
 
224
277
  /** Shared, read-only context every capability factory receives. */
@@ -231,6 +284,12 @@ interface BaseContext {
231
284
 
232
285
  /** What `setup` receives: the run's lifecycle, observe-only. */
233
286
  export interface PluginSetupContext extends BaseContext {
287
+ /** This plugin's entries in the workspace's cache (`PluginCache`). */
288
+ readonly cache: PluginCache
289
+ /** The run is on CI (`CI` set, or a provider core detects). */
290
+ readonly ci: boolean
291
+ /** The run's branch (`currentBranch`): `run.branch`, else git's, else the CI provider's. */
292
+ readonly branch: string | null
234
293
  /**
235
294
  * Subscribe a lifecycle handler; it leaves with the run. A throw or a
236
295
  * rejection warns once and switches the plugin's handlers off for the run.
@@ -248,7 +307,7 @@ export interface PluginCommand {
248
307
  export interface CommandContext extends BaseContext {
249
308
  /**
250
309
  * The worker count a `vx run` here uses unless `--concurrency` says
251
- * otherwise: `vx.workspace`'s `concurrency`, else what the machine allows.
310
+ * otherwise: `.vx/workspace`'s `concurrency`, else what the machine allows.
252
311
  * A verb that budgets a run (`vx history`) reads it here; its own
253
312
  * `machineParallelism()` said 4 cores beside `concurrency: 1` (item 1068).
254
313
  */
@@ -256,10 +315,16 @@ export interface CommandContext extends BaseContext {
256
315
  /**
257
316
  * The argv that runs this vx again: the binary alone when compiled, the
258
317
  * runtime and its entry script from source. A verb that runs tasks
259
- * (`vx mcp`'s `runTasks`) spawns `[...vx, 'run', …]` rather than guess
318
+ * spawns `[...vx, 'run', …]` rather than guess
260
319
  * which `vx` a PATH holds.
261
320
  */
262
321
  readonly vx: readonly string[]
322
+ /**
323
+ * This plugin's entries in the workspace's cache, as `setup` reaches them
324
+ * (`PluginCache`), read-only: the local store, then each remote the
325
+ * workspace's cache plugins supply, under the run's scope. Opened on first use.
326
+ */
327
+ readonly cache: PluginCache
263
328
  }
264
329
 
265
330
  /** `config` runs before the cache dir is known — it may be what the hook changes. */
@@ -271,7 +336,7 @@ export interface WorkspaceHookContext {
271
336
  export interface DiscoverContext extends WorkspaceHookContext {
272
337
  /** Where vx's cache lives: a plugin may keep what it read to answer here. */
273
338
  readonly cacheDir: string
274
- /** The projects found so far: core's, then earlier plugins'. */
339
+ /** The projects found so far: the lock's, then earlier plugins'. */
275
340
  readonly projects: readonly ProjectMeta[]
276
341
  /**
277
342
  * What `git status` lists in the workspace, workspace-relative: modified,
@@ -283,30 +348,46 @@ export interface DiscoverContext extends WorkspaceHookContext {
283
348
  worktreeChanges(): Promise<readonly string[] | null>
284
349
  }
285
350
 
286
- export interface NamedProject {
351
+ /** What a `discover` hook returns per directory — see `VxPlugin.discover`. */
352
+ export interface DiscoveredProject {
353
+ /** Absolute or relative to the workspace root. */
287
354
  readonly dir: string
288
- readonly name: string
355
+ /**
356
+ * Its name, when its config names none: replaces a path name, never a
357
+ * config's; a different name from an earlier plugin is refused. Unset on
358
+ * a new directory: its root-relative path.
359
+ */
360
+ readonly name?: string
361
+ /** Directories this project builds after: `^task` order, and reach for `--filter` and `--affected`. */
362
+ readonly deps?: readonly string[]
363
+ /** Directories a change reaches this project through (`--filter`, `--affected`), with no order. */
364
+ readonly reach?: readonly string[]
289
365
  }
290
366
 
291
367
  export interface ProjectHookContext extends BaseContext {
292
- /** The package name (`package.json#name`). */
368
+ /** The project's name (`ProjectMeta.name`). */
293
369
  readonly name: string
294
370
  /** The project's directory, absolute. */
295
371
  readonly dir: string
296
- /** The parsed `package.json`, read-only. */
297
- readonly packageJson: Readonly<Record<string, unknown>>
298
372
  /**
299
- * Every package the workspace discovered — the one being visited, the
300
- * rest of the run's scope and the packages outside it, config file or
301
- * not — as core read them at startup. A plugin whose mapping needs the
302
- * whole workspace (a `dependsOn` is only valid against every package's
303
- * scripts at once) reads it here instead of walking the workspace a
373
+ * Every project the workspace discovered — the one being visited, the
374
+ * rest of the run's scope and the projects outside it, config file or
375
+ * not — as the `discover` stage left them. A plugin whose mapping needs
376
+ * the whole workspace (a `dependsOn` is only valid against every
377
+ * package's scripts at once) reads it here instead of walking the workspace a
304
378
  * second time. The same array is handed to every visit of a run and a
305
379
  * new one to the next run — a plugin instance outlives a run under `vx
306
380
  * watch`, so a per-run memo keys on this array's identity, never on the
307
381
  * process.
308
382
  */
309
383
  readonly projects: readonly ProjectMeta[]
384
+ /**
385
+ * True when `vx lock` (or `--check`) visits: the one pass that checks the
386
+ * configs, so the place to report what a plugin's mapping could not say.
387
+ * A run checks nothing (the lock model), so such a report there repeats
388
+ * on every run.
389
+ */
390
+ readonly lock: boolean
310
391
  }
311
392
 
312
393
  export interface GraphHookContext extends BaseContext {
@@ -320,16 +401,16 @@ export interface GraphHookContext extends BaseContext {
320
401
 
321
402
  export interface KeyHookContext extends BaseContext {}
322
403
 
323
- /** A plugin's claim on workspace fingerprint files — see `VxPlugin.fingerprint`. */
404
+ /** A plugin's claim on workspace-root files — see `VxPlugin.fingerprint`. */
324
405
  export interface FingerprintClaim {
325
- /** Bare names at the workspace root: a lockfile core folds (`pnpm-lock.yaml`) or a file the plugin reads (`turbo.json`). */
406
+ /** Bare names at the workspace root the plugin reads: a lockfile (`pnpm-lock.yaml`), a settings file, `turbo.json`. */
326
407
  readonly files: readonly string[]
327
408
  /**
328
- * The projects (package names) a change to a claimed file affects. Called
409
+ * The projects (by name) a change to a claimed file affects. Called
329
410
  * by `--affected` with the file's bytes at the base ref and in the
330
411
  * working tree (`null` where it does not exist). Return `undefined` to
331
412
  * select every project — the answer a claimant gives when it cannot
332
- * tell, and what core does for an unclaimed file.
413
+ * tell.
333
414
  */
334
415
  affected(
335
416
  change: FingerprintChange,
@@ -345,7 +426,7 @@ export interface FingerprintChange {
345
426
  }
346
427
 
347
428
  export interface FingerprintContext extends BaseContext {
348
- /** Every project in the workspace: package name and absolute directory. */
429
+ /** Every project in the workspace: name and absolute directory. */
349
430
  readonly projects: ReadonlyArray<{ readonly name: string; readonly dir: string }>
350
431
  }
351
432
 
@@ -364,13 +445,29 @@ export interface AdmitContext {
364
445
  export interface ScheduleHookContext extends BaseContext {
365
446
  /** The run's local cache handle — its `dbHandle()` holds the run history a policy can learn from. */
366
447
  readonly localCache: Cache
448
+ /** The run's branch (`currentBranch`), the timings a forecast reads first: pass it to `LocalHistoryProvider`. */
449
+ readonly branch: string | null
450
+ /** This plugin's entries in the workspace's cache, as `setup` has them. */
451
+ readonly cache: PluginCache
367
452
  }
368
453
 
454
+ /** A `cache` hook's context: the workspace and the run's scope, never the local store or the policy, which core owns. */
369
455
  export interface CacheContext extends BaseContext {
370
- /** The local Cache handle the plugin may wrap (LayeredCache(local, remote)). */
371
- readonly localCache: Cache
372
- /** The run's cache policy (the 4 read/write axes). */
373
- readonly policy: CachePolicy
456
+ /**
457
+ * The run's `cache.scope` (`'pr-123'`), `undefined` when none. Core already keys a scoped run's
458
+ * writes apart; a remote reads it to pick a credential or check a token's
459
+ * claims (an OIDC branch) for every call it makes.
460
+ */
461
+ readonly scope: string | undefined
462
+ }
463
+
464
+ export interface OutputContext extends BaseContext {
465
+ /** Core would write to a terminal: stdout (stderr under `json`) is a TTY. */
466
+ readonly isTTY: boolean
467
+ /** The run is on CI (`CI` set, or a provider core detects). */
468
+ readonly ci: boolean
469
+ /** `--format json`: stdout carries the machine-readable result. */
470
+ readonly json: boolean
374
471
  }
375
472
 
376
473
  export interface ExecutorContext extends BaseContext {
@@ -585,6 +682,12 @@ export interface InstallPluginsArgs {
585
682
  * `run:status` channel.
586
683
  */
587
684
  warn?: (message: string) => void
685
+ /** Each plugin's `ctx.cache`, by plugin name. Default: a cache with no layer. */
686
+ cache?: (plugin: string) => PluginCache
687
+ /** Default false. */
688
+ ci?: boolean
689
+ /** Default null. */
690
+ branch?: string | null
588
691
  }
589
692
 
590
693
  /**
@@ -641,6 +744,9 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
641
744
  workspaceRoot,
642
745
  cacheDir,
643
746
  warn,
747
+ cache: args.cache?.(plugin.name) ?? absentPluginCache,
748
+ ci: args.ci ?? false,
749
+ branch: args.branch ?? null,
644
750
  // A subscription made past the hooks leaves with the run like a
645
751
  // hook's does: the bus can outlive the run (`RunOptions.bus`), and
646
752
  // one left behind hears every later run on it (item 635).