@vzn/vx 0.0.635 → 0.0.637
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 +47 -19
- package/docs/caching.md +222 -210
- package/docs/cli.md +699 -664
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +31 -32
- package/docs/schema.md +418 -202
- 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 +2 -2
- package/schemas/show.json +13 -4
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -61
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +123 -144
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +191 -71
- 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 +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +25 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- 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 +192 -70
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +43 -28
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +114 -73
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- 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-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +127 -88
- 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 +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -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/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +251 -241
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +22 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- 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 +362 -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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The workspace doctor's facts — what `vx info` prints and
|
|
2
|
-
//
|
|
1
|
+
// The workspace doctor's facts — what `vx info` prints and its JSON
|
|
2
|
+
// returns: versions, the git status cache, the
|
|
3
3
|
// workspace's shape, the plugins and the seams each fills, what a run
|
|
4
4
|
// will use (workers, memory) and where those come from, the cache's
|
|
5
5
|
// versions and state, the flaky tasks, the lock. One collector, so the
|
|
@@ -13,32 +13,27 @@ import {
|
|
|
13
13
|
cgroupCpuQuota,
|
|
14
14
|
cgroupMemoryLimitBytes,
|
|
15
15
|
executablePath,
|
|
16
|
+
gitEnv,
|
|
16
17
|
isUnsupportedBun,
|
|
18
|
+
isUserError,
|
|
17
19
|
machineMemoryBytes,
|
|
18
20
|
machineParallelism,
|
|
19
|
-
maskedLine,
|
|
20
21
|
parseSize,
|
|
21
|
-
|
|
22
|
+
treeBytes,
|
|
22
23
|
} from '../util/index.js'
|
|
24
|
+
import { vxPath } from '../handoff.js'
|
|
23
25
|
import { VERSION } from '../version.js'
|
|
24
26
|
import { probeSandbox, resetSandbox, untracedReason } from '../exec/index.js'
|
|
25
27
|
import {
|
|
26
|
-
computeWorkspaceFingerprint,
|
|
27
28
|
findWorkspaceRoot,
|
|
28
|
-
type LoadReads,
|
|
29
|
-
loadWorkspace,
|
|
30
29
|
lockfilePath,
|
|
31
30
|
resolveCacheDir,
|
|
32
31
|
resolveStoreRoot,
|
|
33
32
|
} from '../workspace/index.js'
|
|
34
33
|
import { flakyTasks, type FlakyTask } from './failure-mode.js'
|
|
35
34
|
import type { VxPlugin } from './plugin.js'
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
type LoadProjectsArgs,
|
|
39
|
-
loadProjects,
|
|
40
|
-
loadWorkspacePlugins,
|
|
41
|
-
} from './projects.js'
|
|
35
|
+
import { discoverProjects, loadProjects, loadWorkspacePlugins, lockReader } from './projects.js'
|
|
36
|
+
import { atPath, envName, resolveSetting, SETTINGS, type SettingSource } from '../settings.js'
|
|
42
37
|
|
|
43
38
|
const warnToStderr = (message: string): void => {
|
|
44
39
|
process.stderr.write(`${message}\n`)
|
|
@@ -47,6 +42,10 @@ const warnToStderr = (message: string): void => {
|
|
|
47
42
|
/** The doctor's facts, typed: what `--format json` prints and the pretty rows render. */
|
|
48
43
|
export interface InfoFacts {
|
|
49
44
|
vx: string
|
|
45
|
+
/** The file that ran: the compiled binary, or the source entry. */
|
|
46
|
+
vxPath: string
|
|
47
|
+
/** The vx that handed this call to the project's own (`VX_HANDED_OFF`), else null. */
|
|
48
|
+
handedOffFrom: string | null
|
|
50
49
|
bun: string
|
|
51
50
|
/**
|
|
52
51
|
* False when `bun` is below `MIN_BUN`. A separate field on purpose: `bun`
|
|
@@ -62,23 +61,16 @@ export interface InfoFacts {
|
|
|
62
61
|
workspaceRoot: string
|
|
63
62
|
projects: number
|
|
64
63
|
tasks: number
|
|
65
|
-
/**
|
|
66
|
-
* The project configs that did not load, each with the loader's own
|
|
67
|
-
* message (the path stripped). A broken config counts as zero tasks
|
|
68
|
-
* rather than failing the doctor — and the doctor says which, since a
|
|
69
|
-
* `0 tasks` that hides a typo is the one fact a bug report needs.
|
|
70
|
-
*/
|
|
71
|
-
configErrors: Array<{ path: string; message: string }>
|
|
72
64
|
plugins: Array<{ name: string; seams: string[] }>
|
|
73
65
|
/**
|
|
74
|
-
* The worker count a run defaults to and where it comes from:
|
|
75
|
-
*
|
|
66
|
+
* The worker count a run defaults to and where it comes from:
|
|
67
|
+
* `run.concurrency` (variable, then workspace), else the cores this process may use — the
|
|
76
68
|
* CPU count capped by a cgroup quota (`cpuQuota`, in cores, null when
|
|
77
69
|
* none binds).
|
|
78
70
|
*/
|
|
79
71
|
workers: {
|
|
80
72
|
count: number
|
|
81
|
-
source: 'workspace' | 'cgroup' | 'cores'
|
|
73
|
+
source: 'env' | 'workspace' | 'cgroup' | 'cores'
|
|
82
74
|
cores: number
|
|
83
75
|
cpuQuota: number | null
|
|
84
76
|
}
|
|
@@ -88,13 +80,26 @@ export interface InfoFacts {
|
|
|
88
80
|
*/
|
|
89
81
|
memory: { usableBytes: number; totalBytes: number; cgroupLimitBytes: number | null }
|
|
90
82
|
cacheDir: string
|
|
83
|
+
/** Bytes on disk under `cacheDir`: the index and run history, and the entries when no store holds them. */
|
|
84
|
+
cacheDirBytes: number
|
|
91
85
|
/** The shared store holding the entries, or null when `cacheDir` holds them. */
|
|
92
86
|
cacheStore: string | null
|
|
87
|
+
/** Bytes on disk under `cacheStore`, shared by the repo's checkouts; null with no store. */
|
|
88
|
+
cacheStoreBytes: number | null
|
|
93
89
|
cacheVersion: string
|
|
94
90
|
schemaVersion: string
|
|
95
91
|
cacheEntries: number
|
|
96
92
|
cacheBytes: number
|
|
97
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Every setting (src/settings.ts) as a run with no flags takes it: its
|
|
95
|
+
* variable's value, else the workspace's (after the `config` stage),
|
|
96
|
+
* else null with source `default` (docs/schema.md names each default).
|
|
97
|
+
*/
|
|
98
|
+
settings: Record<
|
|
99
|
+
string,
|
|
100
|
+
{ value: string | number | boolean | null; source: 'env' | 'workspace' | 'default' }
|
|
101
|
+
>
|
|
102
|
+
/** `cache.retention`, with `maxSize` in bytes; null when none is set. */
|
|
98
103
|
cacheRetention: { olderThan?: string; maxSize?: string; maxBytes?: number } | null
|
|
99
104
|
orphans: { artifacts: number; bytes: number }
|
|
100
105
|
runs24h: number
|
|
@@ -126,10 +131,9 @@ export interface CollectInfoOptions {
|
|
|
126
131
|
|
|
127
132
|
export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): Promise<InfoFacts> {
|
|
128
133
|
const warn = opts.warn ?? warnToStderr
|
|
129
|
-
const reads: LoadReads = new Map()
|
|
130
134
|
// Both first: outside a workspace, or under a workspace config that does
|
|
131
135
|
// not load, the doctor refuses at once, with no probe (~180 ms) ahead.
|
|
132
|
-
const root = await findWorkspaceRoot(cwd
|
|
136
|
+
const root = await findWorkspaceRoot(cwd)
|
|
133
137
|
const workspace = await loadWorkspacePlugins(root, warn)
|
|
134
138
|
// The sandbox probe asks nothing of the workspace, so it runs under the
|
|
135
139
|
// loads below instead of after them. Awaited on every path: it resets
|
|
@@ -138,7 +142,7 @@ export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): P
|
|
|
138
142
|
// Rejects only if the reset does; awaited below, never unhandled.
|
|
139
143
|
probing.catch(() => {})
|
|
140
144
|
try {
|
|
141
|
-
return await collectWorkspaceInfo(cwd, root,
|
|
145
|
+
return await collectWorkspaceInfo(cwd, root, workspace, opts, probing)
|
|
142
146
|
} catch (err) {
|
|
143
147
|
// The workspace's refusal is the error worth reporting.
|
|
144
148
|
await probing.catch(() => {})
|
|
@@ -149,7 +153,6 @@ export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): P
|
|
|
149
153
|
async function collectWorkspaceInfo(
|
|
150
154
|
cwd: string,
|
|
151
155
|
root: string,
|
|
152
|
-
reads: LoadReads,
|
|
153
156
|
{ workspaceConfig, plugins }: Awaited<ReturnType<typeof loadWorkspacePlugins>>,
|
|
154
157
|
opts: CollectInfoOptions,
|
|
155
158
|
probing: Promise<Omit<InfoFacts['sandbox'], 'declared'>>,
|
|
@@ -159,7 +162,17 @@ async function collectWorkspaceInfo(
|
|
|
159
162
|
opts.cacheDir === undefined
|
|
160
163
|
? resolveCacheDir(root, workspaceConfig)
|
|
161
164
|
: path.resolve(cwd, opts.cacheDir)
|
|
162
|
-
|
|
165
|
+
// The lock's projects: none without a lock, or with one a run would
|
|
166
|
+
// refuse, which is `vx lock`'s to report.
|
|
167
|
+
const lock = lockReader(root)
|
|
168
|
+
const held = await lock().then(
|
|
169
|
+
() => true,
|
|
170
|
+
(err: unknown) => {
|
|
171
|
+
if (!isUserError(err)) throw err
|
|
172
|
+
return false
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
const metas = held ? await discoverProjects(root, lock, plugins, cacheDir, warn) : []
|
|
163
176
|
const storeRoot =
|
|
164
177
|
opts.cacheDir === undefined ? await resolveStoreRoot(root, workspaceConfig) : null
|
|
165
178
|
const cache = new Cache(cacheDir, undefined, undefined, undefined, 'inspect', storeRoot)
|
|
@@ -168,45 +181,34 @@ async function collectWorkspaceInfo(
|
|
|
168
181
|
let flaky: FlakyTask[]
|
|
169
182
|
let taskCount = 0
|
|
170
183
|
let sandboxed = 0
|
|
171
|
-
let configErrors: InfoFacts['configErrors'] = []
|
|
172
184
|
try {
|
|
173
185
|
stats = cache.stats()
|
|
174
186
|
orphans = await cache.orphanStats()
|
|
175
187
|
flaky = flakyTasks(cache.dbHandle())
|
|
176
|
-
// The run path's load — a plugin's `project` stage
|
|
177
|
-
// doctor's task count is the number a run would see.
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
const load = {
|
|
181
|
-
workspaceRoot: root,
|
|
182
|
-
cacheDir,
|
|
183
|
-
plugins,
|
|
184
|
-
projectMetas: metas,
|
|
185
|
-
closure: false,
|
|
186
|
-
lock: null,
|
|
187
|
-
evalCache: {
|
|
188
|
-
store: cache,
|
|
189
|
-
workspaceRoot: root,
|
|
190
|
-
workspaceFingerprint: await computeWorkspaceFingerprint(root, reads),
|
|
191
|
-
},
|
|
192
|
-
warn,
|
|
193
|
-
} as const
|
|
188
|
+
// The run path's load — the lock, a plugin's `project` stage counted —
|
|
189
|
+
// so the doctor's task count is the number a run would see. No lock, or
|
|
190
|
+
// one a run would refuse, counts zero: broken configs are `vx lock`'s
|
|
191
|
+
// to report.
|
|
194
192
|
try {
|
|
195
|
-
const loaded = await loadProjects({
|
|
193
|
+
const loaded = await loadProjects({
|
|
194
|
+
workspaceRoot: root,
|
|
195
|
+
cacheDir,
|
|
196
|
+
plugins,
|
|
197
|
+
projectMetas: metas,
|
|
198
|
+
seeds: 'all',
|
|
199
|
+
closure: false,
|
|
200
|
+
configs: lock,
|
|
201
|
+
warn,
|
|
202
|
+
})
|
|
196
203
|
for (const p of loaded.projects.values()) {
|
|
197
204
|
const tasks = p.config.tasks ?? {}
|
|
198
205
|
taskCount += Object.keys(tasks).length
|
|
199
206
|
for (const t of Object.values(tasks)) if (t?.exec?.sandbox !== undefined) sandboxed++
|
|
200
207
|
}
|
|
201
|
-
} catch {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
;({
|
|
206
|
-
tasks: taskCount,
|
|
207
|
-
sandboxed,
|
|
208
|
-
errors: configErrors,
|
|
209
|
-
} = await countLoadableTasks(load, root))
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (!isUserError(err)) throw err
|
|
210
|
+
taskCount = 0
|
|
211
|
+
sandboxed = 0
|
|
210
212
|
}
|
|
211
213
|
} finally {
|
|
212
214
|
cache.close()
|
|
@@ -217,8 +219,11 @@ async function collectWorkspaceInfo(
|
|
|
217
219
|
const git = gitVersion()
|
|
218
220
|
const statusCache = gitStatusCache(root)
|
|
219
221
|
const sandbox = { ...(await probing), declared: sandboxed }
|
|
222
|
+
const cacheStore = cache.storeDir ?? storeRoot
|
|
220
223
|
return {
|
|
221
224
|
vx: VERSION,
|
|
225
|
+
vxPath: vxPath(),
|
|
226
|
+
handedOffFrom: process.env['VX_HANDED_OFF'] ?? null,
|
|
222
227
|
// An unsupported Bun does not stop a run, it makes the run's ANSWERS
|
|
223
228
|
// wrong (util/bun-version.ts measures which). The row a reader already
|
|
224
229
|
// consults for "is my setup sane" is where that belongs: a warning on
|
|
@@ -235,17 +240,19 @@ async function collectWorkspaceInfo(
|
|
|
235
240
|
workspaceRoot: root,
|
|
236
241
|
projects: metas.length,
|
|
237
242
|
tasks: taskCount,
|
|
238
|
-
configErrors,
|
|
239
243
|
// Which plugins loaded and which seams each fills, in pipeline order —
|
|
240
244
|
// the answer to "why did this task run there / cache there / not at
|
|
241
245
|
// all" before reading any config. A declined seam still costs nothing;
|
|
242
246
|
// this names the declarations, not what a run consulted.
|
|
243
247
|
plugins: plugins.map((p) => ({ name: p.name, seams: filledSeams(p) })),
|
|
244
|
-
workers: workersFact(workspaceConfig
|
|
248
|
+
workers: workersFact(resolveSetting('run.concurrency', workspaceConfig)),
|
|
245
249
|
memory: memoryFact(),
|
|
246
250
|
cacheDir,
|
|
251
|
+
// On disk, not the index's sum: the size `vx cache clean` would free.
|
|
252
|
+
cacheDirBytes: treeBytes(cacheDir),
|
|
247
253
|
// The store the index records, else the one a first run would open.
|
|
248
|
-
cacheStore
|
|
254
|
+
cacheStore,
|
|
255
|
+
cacheStoreBytes: cacheStore === null ? null : treeBytes(cacheStore),
|
|
249
256
|
// The two versions a bug report needs and the reset notice names: the
|
|
250
257
|
// key prefix (a bump orphans every entry) and the index schema (a
|
|
251
258
|
// mismatch drops every table).
|
|
@@ -253,9 +260,10 @@ async function collectWorkspaceInfo(
|
|
|
253
260
|
schemaVersion: SCHEMA_VERSION,
|
|
254
261
|
cacheEntries: stats.entryCount,
|
|
255
262
|
cacheBytes: stats.totalBytes,
|
|
256
|
-
|
|
263
|
+
settings: settingsFact(workspaceConfig),
|
|
264
|
+
cacheRetention: retentionFact(workspaceConfig),
|
|
257
265
|
// A row-less artifact is indexed again only when its task asks for its
|
|
258
|
-
// key; the rest only `vx cache
|
|
266
|
+
// key; the rest only `vx cache clean` reclaims (after an upgrade's
|
|
259
267
|
// schema reset, most often).
|
|
260
268
|
orphans: { artifacts: orphans.orphans, bytes: orphans.orphanBytes },
|
|
261
269
|
runs24h: stats.runCountLast24h,
|
|
@@ -313,11 +321,11 @@ function filledSeams(p: VxPlugin): string[] {
|
|
|
313
321
|
* numbers would be the OOM killer's. The doctor says which one a run
|
|
314
322
|
* reads, because nothing else would.
|
|
315
323
|
*/
|
|
316
|
-
function workersFact(
|
|
324
|
+
function workersFact(set: { value: unknown; source: SettingSource }): InfoFacts['workers'] {
|
|
317
325
|
const cores = Math.max(1, navigator.hardwareConcurrency)
|
|
318
326
|
const quota = process.platform === 'linux' ? (cgroupCpuQuota() ?? null) : null
|
|
319
|
-
if (
|
|
320
|
-
return { count:
|
|
327
|
+
if (set.source === 'env' || set.source === 'workspace') {
|
|
328
|
+
return { count: set.value as number, source: set.source, cores, cpuQuota: quota }
|
|
321
329
|
}
|
|
322
330
|
const machine = machineParallelism()
|
|
323
331
|
return { count: machine, source: machine < cores ? 'cgroup' : 'cores', cores, cpuQuota: quota }
|
|
@@ -339,6 +347,7 @@ function gitStatusCache(root: string): InfoFacts['gitStatusCache'] {
|
|
|
339
347
|
try {
|
|
340
348
|
const p = Bun.spawnSync({
|
|
341
349
|
cmd: [executablePath('git'), 'config', '--get-regexp', '^core\\.(fsmonitor|untrackedcache)$'],
|
|
350
|
+
env: gitEnv(),
|
|
342
351
|
cwd: root,
|
|
343
352
|
stdout: 'pipe',
|
|
344
353
|
stderr: 'pipe',
|
|
@@ -358,6 +367,7 @@ function gitVersion(): string | null {
|
|
|
358
367
|
try {
|
|
359
368
|
const p = Bun.spawnSync({
|
|
360
369
|
cmd: [executablePath('git'), '--version'],
|
|
370
|
+
env: gitEnv(),
|
|
361
371
|
stdout: 'pipe',
|
|
362
372
|
stderr: 'pipe',
|
|
363
373
|
})
|
|
@@ -371,52 +381,30 @@ function gitVersion(): string | null {
|
|
|
371
381
|
}
|
|
372
382
|
}
|
|
373
383
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const declared = Object.values(loaded.projects.get(meta.name)?.config.tasks ?? {})
|
|
388
|
-
tasks += declared.length
|
|
389
|
-
for (const t of declared) if (t?.exec?.sandbox !== undefined) sandboxed++
|
|
390
|
-
} catch (err) {
|
|
391
|
-
// Counted as zero, and named: the loader's message opens with the
|
|
392
|
-
// absolute path — bare for a schema refusal, behind `Project config`
|
|
393
|
-
// for an import that failed, and with `:line:col` after it for a
|
|
394
|
-
// syntax error — which the row carries workspace-relative instead,
|
|
395
|
-
// once (the second form named the file twice, 2026-09-16; the third
|
|
396
|
-
// still did).
|
|
397
|
-
const raw = err instanceof Error ? err.message : String(err)
|
|
398
|
-
const bare = raw.startsWith('Project config ') ? raw.slice('Project config '.length) : raw
|
|
399
|
-
const where = meta.configPath ?? meta.dir
|
|
400
|
-
const message = bare.startsWith(`${where}:`)
|
|
401
|
-
? bare.slice(where.length + 1).trimStart()
|
|
402
|
-
: raw
|
|
403
|
-
errors.push({
|
|
404
|
-
path: relPosix(root, where),
|
|
405
|
-
// A config's own throw is its author's text, a secret too (L-11).
|
|
406
|
-
message: maskedLine(message),
|
|
407
|
-
})
|
|
408
|
-
}
|
|
409
|
-
}),
|
|
410
|
-
)
|
|
411
|
-
errors.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
412
|
-
return { tasks, sandboxed, errors }
|
|
384
|
+
function settingsFact(config: WorkspaceConfig | null): InfoFacts['settings'] {
|
|
385
|
+
const out: InfoFacts['settings'] = {}
|
|
386
|
+
for (const s of SETTINGS) {
|
|
387
|
+
const env = process.env[envName(s.path)]
|
|
388
|
+
const declared = s.config === false ? undefined : atPath(config, s.path)
|
|
389
|
+
out[s.path] =
|
|
390
|
+
env !== undefined && env !== ''
|
|
391
|
+
? { value: env, source: 'env' }
|
|
392
|
+
: declared !== undefined
|
|
393
|
+
? { value: declared as string | number | boolean, source: 'workspace' }
|
|
394
|
+
: { value: null, source: 'default' }
|
|
395
|
+
}
|
|
396
|
+
return out
|
|
413
397
|
}
|
|
414
398
|
|
|
415
|
-
function retentionFact(
|
|
416
|
-
|
|
399
|
+
function retentionFact(config: WorkspaceConfig | null): InfoFacts['cacheRetention'] {
|
|
400
|
+
const raw = (path: string): string | undefined =>
|
|
401
|
+
process.env[envName(path)] || (atPath(config, path) as string | undefined)
|
|
402
|
+
const olderThan = raw('cache.retention.olderThan')
|
|
403
|
+
const maxSize = raw('cache.retention.maxSize')
|
|
404
|
+
if (olderThan === undefined && maxSize === undefined) return null
|
|
417
405
|
return {
|
|
418
|
-
...(
|
|
419
|
-
// Validated at load, so it parses.
|
|
420
|
-
...(
|
|
406
|
+
...(olderThan !== undefined ? { olderThan } : {}),
|
|
407
|
+
// Validated before the run (env) or at load (workspace), so it parses.
|
|
408
|
+
...(maxSize !== undefined ? { maxSize, maxBytes: parseSize(maxSize)! } : {}),
|
|
421
409
|
}
|
|
422
410
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `--download` policy: which tasks may leave their outputs in the remote
|
|
1
|
+
// `--cache-download` policy: which tasks may leave their outputs in the remote
|
|
2
2
|
// store instead of landing them on this machine, and which must not.
|
|
3
3
|
//
|
|
4
4
|
// See docs/design/download-policy-cas-cache-2026-08.md. Two decisions live
|
|
@@ -27,7 +27,7 @@ export type DownloadMode = 'eager' | 'deferred' | 'never'
|
|
|
27
27
|
* (the shape the design doc first sketched): that rule fires whenever a
|
|
28
28
|
* sibling reads the project at all, which is every ordinary workspace —
|
|
29
29
|
* `test` reads `src/**` while `build` writes `dist/**` — and would leave
|
|
30
|
-
* `--download=none` with nothing to defer. Comparing the globs' static
|
|
30
|
+
* `--cache-download=none` with nothing to defer. Comparing the globs' static
|
|
31
31
|
* prefixes answers the question actually being asked, and stays
|
|
32
32
|
* conservative in every direction that matters: a leading wildcard yields
|
|
33
33
|
* `.` and reaches everything, a cacheable task with no declared `files`
|
|
@@ -176,7 +176,7 @@ export function resolveDownloadModes(args: {
|
|
|
176
176
|
//
|
|
177
177
|
// SURFACED counts as asked-for. A requested GROUP has no outputs of its
|
|
178
178
|
// own — `markSurfacedDeps` marks the real tasks it chains — so keying on
|
|
179
|
-
// `requested` alone made `vx run ci --download=toplevel` (ci being a
|
|
179
|
+
// `requested` alone made `vx run ci --cache-download=toplevel` (ci being a
|
|
180
180
|
// group) bring home NOTHING, which is the one thing the mode exists to
|
|
181
181
|
// avoid. run.ts already pairs the two flags for the same reason when it
|
|
182
182
|
// decides which persistent children to keep alive.
|
|
@@ -254,6 +254,8 @@ export interface OutcomeView {
|
|
|
254
254
|
timedOut?: true
|
|
255
255
|
/** A persistent task that never became ready, and why (see `TaskOutcome`). */
|
|
256
256
|
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
257
|
+
/** The input that changed while the command ran (see `TaskOutcome`). */
|
|
258
|
+
inputMoved?: string
|
|
257
259
|
/** A skipped task: the failed (or aborted) task at the root of the block
|
|
258
260
|
* (see `TaskOutcome`); absent for fail-fast's skip, which nothing blocked. */
|
|
259
261
|
blockedBy?: string
|
|
@@ -301,11 +303,18 @@ export function outcomeWord(o: Pick<OutcomeView, 'status' | 'restored'>): string
|
|
|
301
303
|
export function outcomeLabel(
|
|
302
304
|
o: Pick<
|
|
303
305
|
OutcomeView,
|
|
304
|
-
|
|
306
|
+
| 'status'
|
|
307
|
+
| 'restored'
|
|
308
|
+
| 'exitCode'
|
|
309
|
+
| 'timedOut'
|
|
310
|
+
| 'sandboxViolations'
|
|
311
|
+
| 'notReady'
|
|
312
|
+
| 'inputMoved'
|
|
313
|
+
| 'blockedBy'
|
|
305
314
|
>,
|
|
306
315
|
): string {
|
|
307
316
|
if (o.status === 'failed')
|
|
308
|
-
return failedLabel(o.exitCode, o.timedOut, o.sandboxViolations, o.notReady)
|
|
317
|
+
return failedLabel(o.exitCode, o.timedOut, o.sandboxViolations, o.notReady, o.inputMoved)
|
|
309
318
|
if (o.status === 'skipped') return skippedLabel(o.blockedBy)
|
|
310
319
|
return outcomeWord(o)
|
|
311
320
|
}
|
|
@@ -330,7 +339,9 @@ export function skippedLabel(blockedBy: string | undefined): string {
|
|
|
330
339
|
* status line, the run report, the Actions annotation): the exit code, and
|
|
331
340
|
* above 128 the signal it stands for — `failed (exit 137, 128 + SIGKILL)` —
|
|
332
341
|
* or, for vx's own timeout, `failed (timed out, exit 143)`, or a persistent
|
|
333
|
-
* task that never became ready, `failed (never ready: timed out, exit 1)
|
|
342
|
+
* task that never became ready, `failed (never ready: timed out, exit 1)`,
|
|
343
|
+
* or an input changed under `inputStability.severity: 'error'`,
|
|
344
|
+
* `failed (src/a.ts changed during the run, exit 1)`; a
|
|
334
345
|
* sandboxed task's violations are counted after: `failed (exit 1, 2 sandbox
|
|
335
346
|
* violations)`.
|
|
336
347
|
*/
|
|
@@ -339,6 +350,7 @@ export function failedLabel(
|
|
|
339
350
|
timedOut?: true,
|
|
340
351
|
sandboxViolations?: number,
|
|
341
352
|
notReady?: 'timeout' | 'exited' | 'spawn',
|
|
353
|
+
inputMoved?: string,
|
|
342
354
|
): string {
|
|
343
355
|
// A sandboxed task that touched what it never declared fails on that
|
|
344
356
|
// alone (its exit is forced to 1 when it was 0): the count is the reason.
|
|
@@ -353,6 +365,9 @@ export function failedLabel(
|
|
|
353
365
|
notReady === 'timeout' ? 'timed out' : notReady === 'exited' ? 'exited' : 'spawn failed'
|
|
354
366
|
return `failed (never ready: ${why}, exit ${exitCode}${violations})`
|
|
355
367
|
}
|
|
368
|
+
// `inputStability.severity: 'error'` forced the 1: the input is the reason.
|
|
369
|
+
if (inputMoved !== undefined)
|
|
370
|
+
return `failed (${inputMoved === '' ? 'an input' : inputMoved} changed during the run, exit ${exitCode}${violations})`
|
|
356
371
|
// A timeout's 143 is vx's own SIGTERM: the reason, not the signal, reads.
|
|
357
372
|
if (timedOut === true) return `failed (timed out, exit ${exitCode}${violations})`
|
|
358
373
|
const signal = exitSignal(exitCode)
|
|
@@ -398,6 +413,7 @@ export function projectOutcome(outcome: TaskOutcome): OutcomeView {
|
|
|
398
413
|
if (outcome.saveMs !== undefined) view.saveMs = outcome.saveMs
|
|
399
414
|
if (outcome.timedOut === true) view.timedOut = true
|
|
400
415
|
if (outcome.notReady !== undefined) view.notReady = outcome.notReady
|
|
416
|
+
if (outcome.inputMoved !== undefined) view.inputMoved = outcome.inputMoved
|
|
401
417
|
if (outcome.blockedBy !== undefined) view.blockedBy = outcome.blockedBy
|
|
402
418
|
if (outcome.restored !== undefined) view.restored = outcome.restored
|
|
403
419
|
if (outcome.sandboxViolations !== undefined) view.sandboxViolations = outcome.sandboxViolations
|
|
@@ -25,7 +25,6 @@ export interface KeyExcludedArgs {
|
|
|
25
25
|
dropped: ReadonlyMap<string, readonly string[]>
|
|
26
26
|
cache: CacheLayer
|
|
27
27
|
workspaceRoot: string
|
|
28
|
-
workspaceFingerprint: string
|
|
29
28
|
forwardArgs?: readonly string[] | undefined
|
|
30
29
|
nestedDirsByProject: ReadonlyMap<string, string[]>
|
|
31
30
|
gitFilesCache: GitFilesCache
|
|
@@ -57,7 +56,6 @@ export async function keyExcludedDependencies(args: KeyExcludedArgs): Promise<vo
|
|
|
57
56
|
node,
|
|
58
57
|
upstream,
|
|
59
58
|
workspaceRoot: args.workspaceRoot,
|
|
60
|
-
workspaceFingerprint: args.workspaceFingerprint,
|
|
61
59
|
cache: args.cache,
|
|
62
60
|
forwardArgs: args.forwardArgs,
|
|
63
61
|
nestedProjectDirs: args.nestedDirsByProject.get(node.projectName) ?? [],
|