@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.
- package/README.md +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/handoff.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// A global vx hands off to the project's own (owner, 2026-10-13), found
|
|
2
|
+
// from the workspace root, the one lookup every verb shares. Here for
|
|
3
|
+
// the compiled binary installed by hand; an npm install's launcher
|
|
4
|
+
// (npm-launcher.sh) walks the same way and `exec`s, which Bun cannot. The
|
|
5
|
+
// walk runs before anything else loads, through `Bun.file(…).exists()`:
|
|
6
|
+
// importing `node:fs` alone was ~1.1 ms of a 4 ms start (2026-10-13).
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The project-local vx to run instead of this one, or null: the workspace
|
|
10
|
+
* root's ({@link workspaceFile}) `node_modules/@vzn/vx`, run through its
|
|
11
|
+
* `.bin/vx`. Null once handed
|
|
12
|
+
* off (one hop), for a vx that is not a standalone compiled binary (source;
|
|
13
|
+
* an npm install, whose launcher already walked), outside a workspace,
|
|
14
|
+
* when the local one is this binary, and when another user owns it.
|
|
15
|
+
*/
|
|
16
|
+
export async function localVx(
|
|
17
|
+
cwd: string,
|
|
18
|
+
execPath: string,
|
|
19
|
+
compiled: boolean,
|
|
20
|
+
uid = process.getuid?.(),
|
|
21
|
+
): Promise<string | null> {
|
|
22
|
+
if (process.env['VX_HANDED_OFF'] !== undefined || !compiled) return null
|
|
23
|
+
if (execPath.includes('/node_modules/')) return null
|
|
24
|
+
const file = await workspaceFile(cwd)
|
|
25
|
+
if (file === null) return null
|
|
26
|
+
const root = workspaceRootOf(file)
|
|
27
|
+
if (!(await Bun.file(`${root}/node_modules/@vzn/vx/package.json`).exists())) return null
|
|
28
|
+
const bin = `${root}/node_modules/.bin/vx`
|
|
29
|
+
const { realpathSync, statSync } = await import('node:fs')
|
|
30
|
+
try {
|
|
31
|
+
const real = realpathSync(bin)
|
|
32
|
+
// One another user planted would run as you.
|
|
33
|
+
if (statSync(real).uid !== uid) return null
|
|
34
|
+
return real === realpathSync(execPath) ? null : bin
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The workspace config's paths under the root, in lookup order: one marks
|
|
42
|
+
* the workspace root, the directory holding its `.vx` (owner, 2026-10-14).
|
|
43
|
+
*/
|
|
44
|
+
export const WORKSPACE_CONFIG_FILENAMES = [
|
|
45
|
+
'.vx/workspace.ts',
|
|
46
|
+
'.vx/workspace.mts',
|
|
47
|
+
'.vx/workspace.js',
|
|
48
|
+
'.vx/workspace.mjs',
|
|
49
|
+
// A CommonJS config (`module.exports = …`), as Vite and TS name theirs: it
|
|
50
|
+
// was ignored without a word (D-86).
|
|
51
|
+
'.vx/workspace.cts',
|
|
52
|
+
'.vx/workspace.cjs',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
/** The workspace root a workspace config marks: the directory above its `.vx`. */
|
|
56
|
+
export function workspaceRootOf(file: string): string {
|
|
57
|
+
return file.slice(0, file.lastIndexOf('/.vx/')) || '/'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const files = new Map<string, Promise<string | null>>()
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The workspace config that marks the root for `dir`: the first
|
|
64
|
+
* `.vx/workspace.*` from `dir` up to the git top-level ({@link gitTop}),
|
|
65
|
+
* inclusive, or to `/` outside git (owner, 2026-10-13: "Workspace file is
|
|
66
|
+
* the ultimate" and "never go out of git root"). Null when there is none.
|
|
67
|
+
* Learned once per directory per run: the hand-off and the workspace root
|
|
68
|
+
* share it (principle 9).
|
|
69
|
+
*/
|
|
70
|
+
export function workspaceFile(dir: string): Promise<string | null> {
|
|
71
|
+
let file = files.get(dir)
|
|
72
|
+
if (file === undefined) {
|
|
73
|
+
file = findWorkspaceFile(dir)
|
|
74
|
+
files.set(dir, file)
|
|
75
|
+
}
|
|
76
|
+
return file
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** vx wrote a workspace config (`vx init`): what was learned before it is stale. */
|
|
80
|
+
export function workspaceFileWritten(): void {
|
|
81
|
+
files.clear()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function findWorkspaceFile(start: string): Promise<string | null> {
|
|
85
|
+
const top = await gitTop(start)
|
|
86
|
+
const passed: string[] = []
|
|
87
|
+
let found: string | null = null
|
|
88
|
+
walk: for (let d = start; ; d = d.slice(0, d.lastIndexOf('/'))) {
|
|
89
|
+
passed.push(d || '/')
|
|
90
|
+
for (const name of WORKSPACE_CONFIG_FILENAMES) {
|
|
91
|
+
const file = `${d}/${name}`
|
|
92
|
+
if (await Bun.file(file).exists()) {
|
|
93
|
+
found = file
|
|
94
|
+
break walk
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (d === '' || d === top) break
|
|
98
|
+
}
|
|
99
|
+
// Every directory walked has the same answer: the loader asks for the root's.
|
|
100
|
+
for (const d of passed) if (!files.has(d)) files.set(d, Promise.resolve(found))
|
|
101
|
+
return found
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const tops = new Map<string, Promise<string | null>>()
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The git top-level holding `dir`, or null outside git: the nearest `.git`
|
|
108
|
+
* up from it, a directory in a clone, a file in a worktree. A submodule's
|
|
109
|
+
* `.git` file points into its superproject's `.git/modules`, so the walk
|
|
110
|
+
* goes on to the superproject's. Learned once per directory per run: the
|
|
111
|
+
* hand-off and the workspace root walk share it (principle 9).
|
|
112
|
+
*/
|
|
113
|
+
export function gitTop(dir: string): Promise<string | null> {
|
|
114
|
+
let top = tops.get(dir)
|
|
115
|
+
if (top === undefined) {
|
|
116
|
+
top = findGitTop(dir)
|
|
117
|
+
tops.set(dir, top)
|
|
118
|
+
}
|
|
119
|
+
return top
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function findGitTop(start: string): Promise<string | null> {
|
|
123
|
+
for (let d = start; ; d = d.slice(0, d.lastIndexOf('/'))) {
|
|
124
|
+
// A file each: `exists()` is false for a directory.
|
|
125
|
+
if (await Bun.file(`${d}/.git/HEAD`).exists()) return d || '/'
|
|
126
|
+
const file = Bun.file(`${d}/.git`)
|
|
127
|
+
if (await file.exists()) {
|
|
128
|
+
const gitdir = await file.text().catch(() => '')
|
|
129
|
+
if (!/(^|[\s/])\.git\/modules\//.test(gitdir)) return d || '/'
|
|
130
|
+
}
|
|
131
|
+
if (d === '') return null
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** This vx's own file: the compiled binary, else the source entry Bun runs. */
|
|
136
|
+
export function vxPath(): string {
|
|
137
|
+
return Bun.main.startsWith('/$bunfs') ? process.execPath : Bun.main
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run `bin` with this process's arguments and stdio, and exit as it does.
|
|
142
|
+
* A signal sent to this vx alone (`kill`, a process manager) is passed on;
|
|
143
|
+
* one the terminal sent reached the whole foreground group already, and a
|
|
144
|
+
* second would read as "stop now" (npm-launcher.cjs, the same rule).
|
|
145
|
+
*/
|
|
146
|
+
export async function handOff(bin: string, args: readonly string[]): Promise<never> {
|
|
147
|
+
const child = Bun.spawn([bin, ...args], {
|
|
148
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
149
|
+
env: { ...process.env, VX_HANDED_OFF: process.execPath },
|
|
150
|
+
})
|
|
151
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
|
|
152
|
+
process.on(sig, () => {
|
|
153
|
+
if (!inForeground()) child.kill(sig)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
const code = await child.exited
|
|
157
|
+
const { constants } = await import('node:os')
|
|
158
|
+
process.exit(child.signalCode === null ? code : 128 + (constants.signals[child.signalCode] ?? 1))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function inForeground(): boolean {
|
|
162
|
+
try {
|
|
163
|
+
const stat = Bun.spawnSync(['ps', '-o', 'pgid=,tpgid=', '-p', String(process.pid)])
|
|
164
|
+
const [pgid, tpgid] = stat.stdout.toString().trim().split(/\s+/)
|
|
165
|
+
return pgid !== undefined && pgid === tpgid
|
|
166
|
+
} catch {
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,19 +9,11 @@
|
|
|
9
9
|
export { VERSION } from './version.js'
|
|
10
10
|
|
|
11
11
|
// Clean error type — user-input failures print a message without a stack.
|
|
12
|
-
|
|
13
|
-
// without it on the façade an integration package writes its own, and that
|
|
14
|
-
// had already happened. A bounds helper whose floor is load-bearing (a
|
|
15
|
-
// fractional SQL LIMIT is a datatype mismatch, not a smaller page) should
|
|
16
|
-
// have one implementation.
|
|
17
|
-
export { clampInt, UserError, isUserError } from './util/index.js'
|
|
12
|
+
export { UserError, isUserError } from './util/index.js'
|
|
18
13
|
// Every hook a plugin may fill, in pipeline order — the one list the
|
|
19
14
|
// loader, the host and `vx info` read; exported so a doc pin outside core
|
|
20
15
|
// (the site's guides) holds its tables to the same list.
|
|
21
16
|
export { PLUGIN_HOOKS, type PluginHook } from './config.js'
|
|
22
|
-
// The workspace doctor's facts (`vx info`), for a host that answers the
|
|
23
|
-
// same question over another surface — `@vzn/vx-mcp`'s `getWorkspaceInfo`.
|
|
24
|
-
export { collectInfo, type CollectInfoOptions, type InfoFacts } from './orchestrator/index.js'
|
|
25
17
|
// The machine as this process may use it — cores and memory capped by the
|
|
26
18
|
// cgroup a container runs under. Core's default worker count reads the
|
|
27
19
|
// first; a policy that budgets memory (`@vzn/vx-schedule-history`) reads
|
|
@@ -31,16 +23,15 @@ export { machineMemoryBytes, machineParallelism } from './util/index.js'
|
|
|
31
23
|
// wildcards (`[` `]` are not; item 667) — for a plugin that turns declared
|
|
32
24
|
// outputs into something else (`@vzn/vx-reapi`'s literal `output_paths`).
|
|
33
25
|
export { isLiteralPattern, normalizeGlob } from './util/index.js'
|
|
34
|
-
// A task's command as vx prints it: values of its `exec.env.secret` names
|
|
35
|
-
// masked. `vx show` and telemetry show this; a plugin that hands a command
|
|
36
|
-
// to someone else (`@vzn/vx-mcp`'s `listTasks`, an AI agent) does too (L-26).
|
|
37
|
-
// `maskedLine` is the same for a line no one task owns: the error `vx mcp`
|
|
38
|
-
// hands an agent (L-11).
|
|
39
|
-
export { maskedCommand, maskedLine } from './util/index.js'
|
|
40
26
|
|
|
41
27
|
// Schema types and helpers (used by user vx.config files and presets).
|
|
42
28
|
export type {
|
|
43
29
|
WorkspaceConfig,
|
|
30
|
+
RunSettings,
|
|
31
|
+
CacheSettings,
|
|
32
|
+
LockSettings,
|
|
33
|
+
WatchSettings,
|
|
34
|
+
Duration,
|
|
44
35
|
ProjectConfig,
|
|
45
36
|
TaskConfig,
|
|
46
37
|
ExecConfig,
|
|
@@ -65,7 +56,7 @@ export { run, planRun, prepareRun } from './orchestrator/index.js'
|
|
|
65
56
|
// The per-task duration history a `schedule` plugin learns from
|
|
66
57
|
// (`@vzn/vx-schedule-history` does; a plugin package reaches core only
|
|
67
58
|
// through this façade, which is what put these here).
|
|
68
|
-
export { LocalHistoryProvider } from './orchestrator/index.js'
|
|
59
|
+
export { currentBranch, LocalHistoryProvider } from './orchestrator/index.js'
|
|
69
60
|
export type {
|
|
70
61
|
HistoryProvider,
|
|
71
62
|
HistoryTable,
|
|
@@ -82,6 +73,7 @@ export type { AffectedChanges } from './workspace/index.js'
|
|
|
82
73
|
export type { RunPlan, PlannedTask } from './orchestrator/index.js'
|
|
83
74
|
export type { CiContext, GitContext, HostContext, WorkspaceIdentity } from './orchestrator/index.js'
|
|
84
75
|
export type {
|
|
76
|
+
CacheFlags,
|
|
85
77
|
CachePolicy,
|
|
86
78
|
Logger,
|
|
87
79
|
OutputView,
|
|
@@ -96,44 +88,48 @@ export { outputsOverlap, splitTaskId } from './graph/index.js'
|
|
|
96
88
|
export type { TaskNode, TaskOutcome, TaskStatus } from './graph/index.js'
|
|
97
89
|
|
|
98
90
|
// Cache classes + the layer interface (the `cache` capability's currency).
|
|
99
|
-
export { Cache
|
|
100
|
-
export type {
|
|
91
|
+
export { Cache } from './cache/index.js'
|
|
92
|
+
export type {
|
|
93
|
+
CacheLayer,
|
|
94
|
+
PluginCache,
|
|
95
|
+
RemoteCacheLayer,
|
|
96
|
+
RunRecord,
|
|
97
|
+
InvocationRecord,
|
|
98
|
+
} from './cache/index.js'
|
|
101
99
|
|
|
102
100
|
// Workspace discovery + the project/config catalog surface — an
|
|
103
101
|
// out-of-process service/CLI needs these. `loadProjectConfig` is the RAW
|
|
104
102
|
// per-file load (`vx lock` freezes exactly that); the resolved view a run
|
|
105
103
|
// or `vx show` sees — plugin stages applied — is `loadResolvedProjects`
|
|
106
|
-
// below.
|
|
107
|
-
//
|
|
108
|
-
export { findWorkspaceRoot } from './workspace/index.js'
|
|
109
|
-
export {
|
|
110
|
-
loadWorkspace,
|
|
111
|
-
loadProjectConfig,
|
|
112
|
-
listProjects as listProjectMetas,
|
|
113
|
-
} from './workspace/index.js'
|
|
104
|
+
// below. `PROJECT_CONFIG_FILENAMES` are the names a project's config may
|
|
105
|
+
// have, in precedence order, for a tool that finds or writes one.
|
|
106
|
+
export { findWorkspaceRoot, PROJECT_CONFIG_FILENAMES } from './workspace/index.js'
|
|
107
|
+
export { loadProjectConfig } from './workspace/index.js'
|
|
114
108
|
export type { ProjectMeta } from './workspace/index.js'
|
|
115
|
-
// The package graph as a run sees it (workspace deps by manifest), for a tool
|
|
116
|
-
// that needs a project's transitive closure the way `vx run` computes it.
|
|
117
|
-
export { buildPackageGraph } from './workspace/index.js'
|
|
118
109
|
// The run path's RESOLVED view — plugin `config` and `project` stages
|
|
119
|
-
// applied
|
|
120
|
-
//
|
|
110
|
+
// applied — for a reader outside the CLI
|
|
111
|
+
// (`@vzn/vx-schedule-history`, an embedder's task catalog). What `vx show`
|
|
121
112
|
// prints.
|
|
122
113
|
export { loadResolvedProjects } from './orchestrator/index.js'
|
|
123
114
|
export type { ProjectEntry } from './workspace/index.js'
|
|
124
115
|
// The migration seam: how a generated config is planned, rendered, guarded
|
|
125
|
-
// and written.
|
|
126
|
-
//
|
|
116
|
+
// and written. `@vzn/vx-migrate` (package.json scripts, Turbo, Nx) and any
|
|
117
|
+
// other adoption tool use it from here; `taskNameProblem` is why a name
|
|
118
|
+
// cannot be a task's, so a mapper leaves such a script out.
|
|
127
119
|
export {
|
|
128
120
|
applyMigration,
|
|
129
121
|
foldScriptHooks,
|
|
130
122
|
PERSISTENT_TODO,
|
|
131
123
|
pruneOrphanPersistentNotes,
|
|
132
124
|
quoteTsLiteral,
|
|
125
|
+
taskNameProblem,
|
|
133
126
|
} from './workspace/index.js'
|
|
127
|
+
// Output components: a plugin draws its lines with core's, so they look alike.
|
|
128
|
+
export { ui } from './util/index.js'
|
|
134
129
|
export type {
|
|
135
130
|
ApplyMigrationArgs,
|
|
136
131
|
MigrationFormat,
|
|
132
|
+
MigrationMember,
|
|
137
133
|
GeneratedProject,
|
|
138
134
|
GeneratedTask,
|
|
139
135
|
MigrationPlan,
|
|
@@ -143,7 +139,7 @@ export type {
|
|
|
143
139
|
// Plugin API — the run-level extension points. Behavior capabilities
|
|
144
140
|
// (executor / cache) change WHAT/HOW work runs; the observe-only `telemetry`
|
|
145
141
|
// capability is the canonical data-export path and cannot change behavior.
|
|
146
|
-
// A plugin is declared in vx
|
|
142
|
+
// A plugin is declared in .vx/workspace.ts via defineWorkspace({ plugins }).
|
|
147
143
|
// See docs/design/observability-architecture-2026-06.md.
|
|
148
144
|
// The claimant's shell for a lockfile plugin (`@vzn/vx-lockfile`):
|
|
149
145
|
// the claim, the per-project key, the memo and the `--affected` diff around
|
|
@@ -153,6 +149,7 @@ export {
|
|
|
153
149
|
reachDigests,
|
|
154
150
|
type LockfileClaimHooks,
|
|
155
151
|
type LockfileClaimOptions,
|
|
152
|
+
type LockfileImporter,
|
|
156
153
|
type ReachGraph,
|
|
157
154
|
} from './orchestrator/index.js'
|
|
158
155
|
export type {
|
|
@@ -162,6 +159,7 @@ export type {
|
|
|
162
159
|
PluginOptionKinds,
|
|
163
160
|
CacheContext,
|
|
164
161
|
ExecutorContext,
|
|
162
|
+
OutputContext,
|
|
165
163
|
CommandContext,
|
|
166
164
|
FingerprintChange,
|
|
167
165
|
FingerprintClaim,
|
|
@@ -174,8 +172,10 @@ export type {
|
|
|
174
172
|
AdmitContext,
|
|
175
173
|
WorkspaceHookContext,
|
|
176
174
|
DiscoverContext,
|
|
177
|
-
|
|
175
|
+
DiscoveredProject,
|
|
178
176
|
PluginSetupContext,
|
|
177
|
+
RunReport,
|
|
178
|
+
RunReportTask,
|
|
179
179
|
PluginHookName,
|
|
180
180
|
PluginHookHandlers,
|
|
181
181
|
RunStartInfo,
|
|
@@ -244,26 +244,3 @@ export type {
|
|
|
244
244
|
|
|
245
245
|
// The serializable run result and its per-task views (what `run()` returns).
|
|
246
246
|
export type { RunResult, TaskView, OutcomeView } from './orchestrator/index.js'
|
|
247
|
-
|
|
248
|
-
// The run-history queries a reader outside the CLI takes today: `vx why`'s
|
|
249
|
-
// answer, a run's failures and a task's log, which `@vzn/vx-mcp` serves. The event bus /
|
|
250
|
-
// wire form and the other history readers (`listRuns`, `getRun`, …) left the
|
|
251
|
-
// façade 2026-09-10 with no consumer; the telemetry seam above is the
|
|
252
|
-
// canonical export path.
|
|
253
|
-
export {
|
|
254
|
-
cacheKeyDiff,
|
|
255
|
-
latestRunId,
|
|
256
|
-
resolveRunId,
|
|
257
|
-
rootCauses,
|
|
258
|
-
runFailures,
|
|
259
|
-
taskLog,
|
|
260
|
-
whyDidThisRerunQuery,
|
|
261
|
-
} from './orchestrator/index.js'
|
|
262
|
-
export type {
|
|
263
|
-
CacheKeyDiff,
|
|
264
|
-
OutputLocation,
|
|
265
|
-
RootCause,
|
|
266
|
-
RunFailures,
|
|
267
|
-
TaskFailure,
|
|
268
|
-
TaskLog,
|
|
269
|
-
} from './orchestrator/index.js'
|
|
@@ -29,7 +29,7 @@ import type { VxPlugin } from './plugin.js'
|
|
|
29
29
|
import { isDefaultBuild } from './projects.js'
|
|
30
30
|
|
|
31
31
|
/** Every file in this set re-keys each of its project's tasks, whatever their inputs. */
|
|
32
|
-
const KEYED_BY_EVERY_TASK = new Set(
|
|
32
|
+
const KEYED_BY_EVERY_TASK: ReadonlySet<string> = new Set(PROJECT_CONFIG_FILENAMES)
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* The projects every task of which the change reaches: the whole ones the
|
|
@@ -41,8 +41,9 @@ const KEYED_BY_EVERY_TASK = new Set(['package.json', ...PROJECT_CONFIG_FILENAMES
|
|
|
41
41
|
function wholeProjects(
|
|
42
42
|
changes: AffectedChanges,
|
|
43
43
|
projects: ReadonlyMap<string, ProjectEntry>,
|
|
44
|
-
):
|
|
45
|
-
|
|
44
|
+
): Map<string, string | undefined> {
|
|
45
|
+
// Each whole project, with the path no cached task claims when one did it.
|
|
46
|
+
const whole = new Map<string, string | undefined>([...changes.whole].map((n) => [n, undefined]))
|
|
46
47
|
for (const [name, rels] of changes.paths) {
|
|
47
48
|
if (whole.has(name)) continue
|
|
48
49
|
const caches = Object.values(projects.get(name)?.config.tasks ?? {}).flatMap((t) =>
|
|
@@ -50,7 +51,8 @@ function wholeProjects(
|
|
|
50
51
|
)
|
|
51
52
|
const unclaimed = (rel: string): boolean =>
|
|
52
53
|
KEYED_BY_EVERY_TASK.has(rel) || !caches.some((c) => declaresInput(c, rel, null))
|
|
53
|
-
|
|
54
|
+
const rel = rels.find(unclaimed)
|
|
55
|
+
if (rel !== undefined) whole.set(name, rel)
|
|
54
56
|
}
|
|
55
57
|
return whole
|
|
56
58
|
}
|
|
@@ -60,13 +62,15 @@ function wholeProjects(
|
|
|
60
62
|
* `input`, a changed `file` is one of its declared inputs; `project`, its
|
|
61
63
|
* project changed and the task reads it whole (an uncached task, a path no
|
|
62
64
|
* cached task declares, or no single `file`: a lockfile claim, a manifest
|
|
63
|
-
* edge, a config import); `
|
|
65
|
+
* edge, a config import); `workspace`, a workspace-wide `file` (the
|
|
66
|
+
* workspace config, a root file a plugin claims) reached every task of its
|
|
67
|
+
* project; `package`, a `^` edge passes through `project`, a
|
|
64
68
|
* changed package with no such task; `named`, asked as `pkg#task`;
|
|
65
69
|
* `selected`, another filter selected its project outright. `via` is the
|
|
66
70
|
* `dependsOn` chain, nearest first, to the task the change seeded.
|
|
67
71
|
*/
|
|
68
72
|
export interface AffectedReason {
|
|
69
|
-
kind: 'input' | 'project' | 'package' | 'named' | 'selected'
|
|
73
|
+
kind: 'input' | 'project' | 'workspace' | 'package' | 'named' | 'selected'
|
|
70
74
|
/** Workspace-relative. */
|
|
71
75
|
file?: string
|
|
72
76
|
project?: string
|
|
@@ -100,7 +104,10 @@ export function affectedRoots(
|
|
|
100
104
|
const seeds = explain === undefined ? null : new Map<string, AffectedReason>()
|
|
101
105
|
const through = explain === undefined ? null : new Map<string, string>()
|
|
102
106
|
const inProject = (name: string, rel: string | undefined): AffectedReason => {
|
|
103
|
-
if (rel === undefined)
|
|
107
|
+
if (rel === undefined) {
|
|
108
|
+
const file = changes.wholeBy?.get(name)
|
|
109
|
+
return file === undefined ? { kind: 'project' } : { kind: 'workspace', file }
|
|
110
|
+
}
|
|
104
111
|
const dir = path.relative(explain!.workspaceRoot, projects.get(name)!.dir)
|
|
105
112
|
return { kind: 'project', file: dir === '' ? rel : path.posix.join(dir, rel) }
|
|
106
113
|
}
|
|
@@ -157,7 +164,7 @@ export function affectedRoots(
|
|
|
157
164
|
return hit
|
|
158
165
|
}
|
|
159
166
|
if (whole.has(n.projectName)) {
|
|
160
|
-
seeds?.set(n.id, inProject(n.projectName,
|
|
167
|
+
seeds?.set(n.id, inProject(n.projectName, whole.get(n.projectName)))
|
|
161
168
|
return true
|
|
162
169
|
}
|
|
163
170
|
const rel = rels.find((r) => declaresInput(cache, r, null))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// When each command of the run ran and where it may write, so
|
|
2
|
+
// `inputStability.strategy: 'mutable'` can tell a task's own identical
|
|
3
|
+
// rewrite from another task's write and restore (STATUS item 28): a file
|
|
4
|
+
// whose bytes match the key after a write is the task's own only if no
|
|
5
|
+
// other command that may write it ran between the describe and that write.
|
|
6
|
+
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import type { WriteReach } from './sandbox-request.js'
|
|
9
|
+
|
|
10
|
+
interface Window {
|
|
11
|
+
id: string
|
|
12
|
+
projectDir: string
|
|
13
|
+
reach: WriteReach
|
|
14
|
+
from: number
|
|
15
|
+
to: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CommandWindows {
|
|
19
|
+
/** A command starts; the returned function marks its end. A server never calls it. */
|
|
20
|
+
open(id: string, projectDir: string, reach: WriteReach): () => void
|
|
21
|
+
/** Another command that may write `file` and ran at some point in [from, to]. */
|
|
22
|
+
writerDuring(self: string, file: string, from: number, to: number): string | undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function commandWindows(): CommandWindows {
|
|
26
|
+
const windows: Window[] = []
|
|
27
|
+
return {
|
|
28
|
+
open(id, projectDir, reach) {
|
|
29
|
+
if (reach === 'none') return () => {}
|
|
30
|
+
const w: Window = { id, projectDir, reach, from: Date.now(), to: Infinity }
|
|
31
|
+
windows.push(w)
|
|
32
|
+
return () => {
|
|
33
|
+
w.to = Date.now()
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
writerDuring(self, file, from, to) {
|
|
37
|
+
for (const w of windows) {
|
|
38
|
+
if (w.id === self || w.from > to || w.to < from) continue
|
|
39
|
+
if (w.reach === 'workspace' || file.startsWith(w.projectDir + path.sep)) return w.id
|
|
40
|
+
}
|
|
41
|
+
return undefined
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The run-scoped registry of tasks whose outputs were left in the remote
|
|
2
|
-
// store (`--download=none`), and the lazy materialisation that fetches
|
|
2
|
+
// store (`--cache-download=none`), and the lazy materialisation that fetches
|
|
3
3
|
// them when a locally-placed task turns out to need them.
|
|
4
4
|
//
|
|
5
5
|
// See docs/design/download-policy-cas-cache-2026-08.md §5. The two
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
// remote store (the executor's own record) is the entry.
|
|
10
10
|
// - Materialisation CONVERGES: after the bytes land, core runs the
|
|
11
11
|
// ordinary `cache.save`, so the machine is indistinguishable from a
|
|
12
|
-
// `--download=all` run and every later reader (get, prune, verify,
|
|
12
|
+
// `--cache-download=all` run and every later reader (get, prune, verify,
|
|
13
13
|
// metrics) sees one entry shape, not two.
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
cleanOutputs,
|
|
17
|
+
committedFiles,
|
|
17
18
|
resolveOutputs,
|
|
18
19
|
type CacheLayer,
|
|
19
20
|
type GitFilesCache,
|
|
@@ -73,7 +74,7 @@ export class DeferredOutputs {
|
|
|
73
74
|
*/
|
|
74
75
|
async materializeFor(node: TaskNode): Promise<void> {
|
|
75
76
|
// Nothing deferred, nothing to find: the closure walk is the size of
|
|
76
|
-
// the graph, and every task of a run without `--download=none` paid it
|
|
77
|
+
// the graph, and every task of a run without `--cache-download=none` paid it
|
|
77
78
|
// (1.4 s of a 3,270-task cold run's main thread).
|
|
78
79
|
if (this.entries.size === 0) return
|
|
79
80
|
const needed: string[] = []
|
|
@@ -116,7 +117,12 @@ export class DeferredOutputs {
|
|
|
116
117
|
// workspace-anchored can reach this path.
|
|
117
118
|
const outputs = producer.config.cache?.outputs.files ?? []
|
|
118
119
|
const nestedProjectDirs = this.args.nestedDirsByProject.get(producer.projectName) ?? []
|
|
119
|
-
const cleanArgs = {
|
|
120
|
+
const cleanArgs = {
|
|
121
|
+
projectDir: producer.projectDir,
|
|
122
|
+
outputs,
|
|
123
|
+
nestedProjectDirs,
|
|
124
|
+
committed: committedFiles(this.args.gitFilesCache, producer.projectDir),
|
|
125
|
+
}
|
|
120
126
|
|
|
121
127
|
// Mirror `restoreHit`'s sequencing so the two paths cannot drift: wipe
|
|
122
128
|
// the declared outputs, write, then tell the git snapshot what moved.
|
|
@@ -135,14 +141,10 @@ export class DeferredOutputs {
|
|
|
135
141
|
} catch (err) {
|
|
136
142
|
const msg = err instanceof Error ? err.message : String(err)
|
|
137
143
|
throw new Error(
|
|
138
|
-
`could not fetch deferred outputs of ${taskId}: ${msg} — re-run it with --force, or use --download=all`,
|
|
144
|
+
`could not fetch deferred outputs of ${taskId}: ${msg} — re-run it with --force, or use --cache-download=all`,
|
|
139
145
|
)
|
|
140
146
|
}
|
|
141
|
-
const outputFiles = await resolveOutputs(
|
|
142
|
-
projectDir: producer.projectDir,
|
|
143
|
-
outputs,
|
|
144
|
-
nestedProjectDirs,
|
|
145
|
-
})
|
|
147
|
+
const outputFiles = await resolveOutputs(cleanArgs)
|
|
146
148
|
this.args.gitFilesCache?.markOutputsChanged(
|
|
147
149
|
producer.projectDir,
|
|
148
150
|
outputFiles.map((f) => f.slice(producer.projectDir.length + 1)),
|