@vzn/vx 0.0.638 → 0.0.639
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/docs/caching.md +44 -15
- package/docs/cli.md +168 -345
- package/docs/execution.md +18 -43
- package/docs/features.md +7 -10
- package/docs/patterns.md +5 -7
- package/docs/schema.md +47 -49
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/skills/vx/SKILL.md +6 -4
- package/src/cache/cache.ts +40 -10
- package/src/cache/git-inputs.ts +6 -0
- package/src/cache/index.ts +2 -2
- package/src/cache/layer.ts +25 -11
- package/src/cache/layered-cache.ts +108 -315
- package/src/cache/plugin-cache.ts +2 -10
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +24 -8
- package/src/cache/run-store.ts +4 -4
- package/src/cache/schema.ts +6 -4
- package/src/cache/scoped-cache.ts +7 -0
- package/src/cli/completions.ts +2 -2
- package/src/cli/foreign-flags.ts +10 -22
- package/src/cli/help.ts +6 -31
- package/src/cli/info.ts +2 -1
- package/src/cli/last.ts +6 -3
- package/src/cli/plan-format.ts +5 -58
- package/src/cli/plugin-commands.ts +2 -2
- package/src/cli/run.ts +23 -185
- package/src/cli/show.ts +6 -0
- package/src/cli/upgrade.ts +123 -39
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch.ts +29 -14
- package/src/cli/why.ts +4 -4
- package/src/config.ts +0 -6
- package/src/exec/executor.ts +1 -1
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/index.ts +9 -1
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +4 -16
- package/src/graph/task-graph.ts +87 -17
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +1 -1
- package/src/{orchestrator → history}/history.ts +1 -1
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +7 -20
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +10 -28
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +21 -386
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +1 -73
- package/src/orchestrator/miss-save.ts +33 -3
- package/src/orchestrator/options.ts +1 -36
- package/src/orchestrator/output-host.ts +4 -3
- package/src/orchestrator/persistent-task.ts +321 -0
- package/src/orchestrator/persistent.ts +130 -3
- package/src/orchestrator/placement.ts +3 -3
- package/src/orchestrator/plan.ts +18 -5
- package/src/orchestrator/plugin-host.ts +67 -47
- package/src/orchestrator/plugin.ts +6 -6
- package/src/orchestrator/prepare.ts +28 -29
- package/src/orchestrator/remote-prefetch.ts +14 -1
- package/src/orchestrator/run-artifacts.ts +5 -177
- package/src/orchestrator/run-context.ts +55 -0
- package/src/orchestrator/run-failures.ts +15 -3
- package/src/orchestrator/run-records.ts +35 -3
- package/src/orchestrator/run-report.ts +81 -179
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +260 -601
- package/src/orchestrator/sandbox-fix.ts +0 -16
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +107 -3
- package/src/orchestrator/steps.ts +50 -0
- package/src/orchestrator/task-hash.ts +3 -16
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +1 -1
- package/src/orchestrator/telemetry.ts +5 -82
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +39 -33
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +158 -20
- package/src/{orchestrator → report}/status-line.ts +84 -7
- package/src/{orchestrator → report}/summary.ts +126 -35
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +1 -22
- package/src/util/colors.ts +44 -4
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +19 -4
- package/src/util/report.ts +13 -5
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +17 -6
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -280
- package/src/orchestrator/tally.ts +0 -112
- /package/src/{orchestrator → report}/path-links.ts +0 -0
- /package/src/{orchestrator → report}/plain-output.ts +0 -0
- /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
1
|
import type { ExecConfig, TaskConfig, CacheConfig, InputStability } from '../config.js'
|
|
3
2
|
import {
|
|
4
3
|
ArtifactVanishedError,
|
|
@@ -16,17 +15,9 @@ import {
|
|
|
16
15
|
FULL_CACHE_POLICY,
|
|
17
16
|
type GitFilesCache,
|
|
18
17
|
racyWindowMs,
|
|
18
|
+
type TaskInputComponent,
|
|
19
19
|
} from '../cache/index.js'
|
|
20
20
|
import {
|
|
21
|
-
buildIsolatedEnv,
|
|
22
|
-
packageManagerPath,
|
|
23
|
-
PM_EXEC_ENV,
|
|
24
|
-
VX_RUN_TASK_ENV,
|
|
25
|
-
VX_RUN_WORKSPACE_ENV,
|
|
26
|
-
runPersistent,
|
|
27
|
-
withForwardArgs,
|
|
28
|
-
releaseBridges,
|
|
29
|
-
wrapSandboxedCommand,
|
|
30
21
|
signalExitCode,
|
|
31
22
|
isLocalExecutor,
|
|
32
23
|
isExecutorFallback,
|
|
@@ -38,7 +29,6 @@ import {
|
|
|
38
29
|
type SandboxViolation,
|
|
39
30
|
type TaskExecutor,
|
|
40
31
|
type TaskInputs,
|
|
41
|
-
PersistentReadyError,
|
|
42
32
|
sandboxReads,
|
|
43
33
|
maskCaptured,
|
|
44
34
|
BoundedCapture,
|
|
@@ -53,7 +43,6 @@ import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '..
|
|
|
53
43
|
import {
|
|
54
44
|
killGraceMs,
|
|
55
45
|
MASKED,
|
|
56
|
-
parseDuration,
|
|
57
46
|
maskedEmitter,
|
|
58
47
|
outputMatcher,
|
|
59
48
|
printable,
|
|
@@ -63,14 +52,13 @@ import {
|
|
|
63
52
|
span,
|
|
64
53
|
UserError,
|
|
65
54
|
} from '../util/index.js'
|
|
66
|
-
import { forwardedSignal, SIGNAL_SHUTDOWN_GRACE_MS
|
|
55
|
+
import { forwardedSignal, SIGNAL_SHUTDOWN_GRACE_MS } from './signals.js'
|
|
67
56
|
import { executorLabel, nameExecutorFailure } from './plugin-host.js'
|
|
68
57
|
import type { CommandWindows } from './command-windows.js'
|
|
69
58
|
import {
|
|
70
59
|
commandWriteReach,
|
|
71
60
|
mayWriteFingerprint,
|
|
72
61
|
type Placeholder,
|
|
73
|
-
placeholderSweeper,
|
|
74
62
|
reachedWithheld,
|
|
75
63
|
sandboxRequestFor,
|
|
76
64
|
sandboxReadable,
|
|
@@ -83,13 +71,13 @@ import {
|
|
|
83
71
|
import { markUnsaved, saveMiss, type OutputDirSnapshot, type SaveFacts } from './miss-save.js'
|
|
84
72
|
import type { FingerprintWatch } from './fingerprint-watch.js'
|
|
85
73
|
import { restoreHit } from './hit-restore.js'
|
|
86
|
-
import type { MissExplainer } from '
|
|
74
|
+
import type { MissExplainer } from '../history/index.js'
|
|
87
75
|
import { shellVerdict } from './shell-verdict.js'
|
|
88
76
|
import { sandboxFix } from './sandbox-fix.js'
|
|
89
77
|
// The hit path's entry stays importable from here (tests).
|
|
90
78
|
export { restoreHit, type RestoreHitArgs } from './hit-restore.js'
|
|
91
79
|
import type { DeferredOutputs } from './deferred-outputs.js'
|
|
92
|
-
import type
|
|
80
|
+
import { type Logger, warnOf } from '../report/index.js'
|
|
93
81
|
import {
|
|
94
82
|
computeGroupHash,
|
|
95
83
|
computeTaskHash,
|
|
@@ -97,11 +85,18 @@ import {
|
|
|
97
85
|
type HashCache,
|
|
98
86
|
type InputFact,
|
|
99
87
|
movedInput,
|
|
100
|
-
type TaskInputComponent,
|
|
101
88
|
type UpfrontGroupKey,
|
|
102
89
|
} from './task-hash.js'
|
|
103
90
|
import { getContext } from './remote-prefetch.js'
|
|
104
91
|
import { expandGroupUpstream, filterUpstreamHashes } from './upstream.js'
|
|
92
|
+
import { executePersistentTask } from './persistent-task.js'
|
|
93
|
+
import {
|
|
94
|
+
forgetUndeclaredWrites,
|
|
95
|
+
isAborted,
|
|
96
|
+
taskBinDirs,
|
|
97
|
+
taskEnv,
|
|
98
|
+
taskTimeoutMs,
|
|
99
|
+
} from './task-process.js'
|
|
105
100
|
|
|
106
101
|
export interface ExecuteArgs {
|
|
107
102
|
node: TaskNode
|
|
@@ -181,8 +176,6 @@ export interface ExecuteArgs {
|
|
|
181
176
|
* workspace. Per-task `exec.timeout` wins. Never hashed.
|
|
182
177
|
*/
|
|
183
178
|
timeout?: number
|
|
184
|
-
/** `run.readyNotice` (ms), resolved like `timeout`; unset, 10 s. */
|
|
185
|
-
readyNotice?: number
|
|
186
179
|
/**
|
|
187
180
|
* The run's `inputStability` layers around the task's own: `run` from
|
|
188
181
|
* the env vars (above the task), `workspace` from `.vx/workspace` (below).
|
|
@@ -283,8 +276,9 @@ export interface ExecuteArgs {
|
|
|
283
276
|
/**
|
|
284
277
|
* Dispatch a single task to one of three execution paths. Each path
|
|
285
278
|
* owns its own outcome shape; the dispatcher just picks based on the
|
|
286
|
-
* task's config shape (group vs persistent vs cached).
|
|
287
|
-
*
|
|
279
|
+
* task's config shape (group vs persistent vs cached). The persistent
|
|
280
|
+
* path is persistent-task.ts; what both spawning paths share is
|
|
281
|
+
* task-process.ts.
|
|
288
282
|
*/
|
|
289
283
|
export async function executeTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
290
284
|
if (isGroupTask(args.node)) return executeGroupTask(args)
|
|
@@ -355,7 +349,7 @@ async function restoreOrMiss(
|
|
|
355
349
|
return await restoreHit({ args, hash, hit, cacheOpStart, taskStartNs })
|
|
356
350
|
} catch (err) {
|
|
357
351
|
if (!(err instanceof ArtifactVanishedError)) throw err
|
|
358
|
-
args.log
|
|
352
|
+
warnOf(args.log)(`[vx] ${args.node.id}: ${err.message} — running it`)
|
|
359
353
|
return null
|
|
360
354
|
}
|
|
361
355
|
}
|
|
@@ -474,294 +468,6 @@ function sameUpstream(live: TaskOutcome[], upfront: TaskOutcome[]): boolean {
|
|
|
474
468
|
return true
|
|
475
469
|
}
|
|
476
470
|
|
|
477
|
-
/**
|
|
478
|
-
* Persistent task: dev server / file watcher / daemon. Spawn, wait
|
|
479
|
-
* for ready (regex match or immediate), stash the subprocess in the
|
|
480
|
-
* orchestrator-owned registry, return success. The orchestrator
|
|
481
|
-
* SIGTERMs the registry at end-of-run.
|
|
482
|
-
*
|
|
483
|
-
* Never reads or writes the cache — the project loader rejects
|
|
484
|
-
* `cache + persistent` at config-load time, so by the time we get
|
|
485
|
-
* here, `cache` is guaranteed undefined.
|
|
486
|
-
*
|
|
487
|
-
* Never routed through an executor: a persistent task is local by
|
|
488
|
-
* construction (its port lives on this machine).
|
|
489
|
-
*/
|
|
490
|
-
async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
491
|
-
const { node, log } = args
|
|
492
|
-
// Type narrowing: the dispatcher checks `exec.persistent` before
|
|
493
|
-
// calling us, so `exec` and `exec.persistent` are both present.
|
|
494
|
-
const step = node.config.exec as ExecConfig & {
|
|
495
|
-
persistent: NonNullable<ExecConfig['persistent']>
|
|
496
|
-
}
|
|
497
|
-
const effectiveForwardArgs = node.requested ? (args.forwardArgs ?? []) : []
|
|
498
|
-
const env = taskEnv(node, step, args.workspaceRoot)
|
|
499
|
-
const wallclockStartNs = process.hrtime.bigint() - args.runStartHrTimeNs
|
|
500
|
-
// Keyed like an uncached task (its whole project), so a cached dependant
|
|
501
|
-
// behind a dev server re-runs when the server's sources change; with no
|
|
502
|
-
// key the dependant folded nothing of it and replayed a stale e2e (A-17).
|
|
503
|
-
const hash =
|
|
504
|
-
args.noDependants === true
|
|
505
|
-
? undefined
|
|
506
|
-
: (args.upfrontKey ??
|
|
507
|
-
(await computeTaskHash({
|
|
508
|
-
node,
|
|
509
|
-
upstream: args.upstream,
|
|
510
|
-
workspaceRoot: args.workspaceRoot,
|
|
511
|
-
cache: args.cache,
|
|
512
|
-
forwardArgs: args.forwardArgs,
|
|
513
|
-
nestedProjectDirs: args.nestedProjectDirs,
|
|
514
|
-
...(args.gitFilesCache !== undefined ? { gitFilesCache: args.gitFilesCache } : {}),
|
|
515
|
-
...(args.hashCache !== undefined ? { hashCache: args.hashCache } : {}),
|
|
516
|
-
})))
|
|
517
|
-
|
|
518
|
-
// The args after `--` reach a server as they reach any task. A readyWhen
|
|
519
|
-
// server once got none — "so the matcher sees the unmodified output",
|
|
520
|
-
// though the args change the command, not what the matcher reads — and
|
|
521
|
-
// `vx run dev -- --port 4000` on the documented Vite task dropped the
|
|
522
|
-
// port in silence (item 1060).
|
|
523
|
-
const plainCommand = withForwardArgs(step.command, effectiveForwardArgs)
|
|
524
|
-
// A persistent task declaring `exec.sandbox` runs inside it like any
|
|
525
|
-
// other: the same grants, the same walls. What it cannot have is the
|
|
526
|
-
// violation REPORT — that reads the trace after the child exits, and a
|
|
527
|
-
// server exits when the run tears it down. Enforced, not reported.
|
|
528
|
-
// (Until 2026-09-09 the block was accepted and silently ignored.)
|
|
529
|
-
let command = plainCommand
|
|
530
|
-
let bridgeTag: string | undefined
|
|
531
|
-
let signalChannel = false
|
|
532
|
-
// The empty files the request pre-created for a literal write grant; a
|
|
533
|
-
// server that never writes one gets it taken back when it exits, and a
|
|
534
|
-
// server that dies before readiness is told the directory spelling — the
|
|
535
|
-
// same trap and the same answer as a one-shot task's (sandbox-request.ts).
|
|
536
|
-
let placeholders: Placeholder[] = []
|
|
537
|
-
if (step.sandbox !== undefined) {
|
|
538
|
-
await args.armSandbox?.()
|
|
539
|
-
// A persistent task never declares `cache` (the loader refuses it), so
|
|
540
|
-
// it keeps the whole linked grant: no key of its can be stale.
|
|
541
|
-
const sb = await sandboxRequestFor(
|
|
542
|
-
node,
|
|
543
|
-
step.sandbox,
|
|
544
|
-
args.workspaceRoot,
|
|
545
|
-
undefined,
|
|
546
|
-
args.nestedProjectDirs,
|
|
547
|
-
args.cacheDir,
|
|
548
|
-
)
|
|
549
|
-
placeholders = sb.placeholders
|
|
550
|
-
// A wrap that refuses (a port the host holds) leaves the server unspawned
|
|
551
|
-
// and the placeholders vx made for it in the project for every later run.
|
|
552
|
-
const wrapped = await wrapSandboxedCommand({
|
|
553
|
-
command: plainCommand,
|
|
554
|
-
cwd: node.projectDir,
|
|
555
|
-
env,
|
|
556
|
-
...sb.sandbox,
|
|
557
|
-
server: true,
|
|
558
|
-
}).catch(async (err: unknown) => {
|
|
559
|
-
await sweepPlaceholders(placeholders)
|
|
560
|
-
throw err
|
|
561
|
-
})
|
|
562
|
-
command = wrapped.wrapped
|
|
563
|
-
bridgeTag = wrapped.tag
|
|
564
|
-
signalChannel = wrapped.forwardsSignals
|
|
565
|
-
}
|
|
566
|
-
// A server's output masked as a one-shot task's is (L-11); `readyWhen`
|
|
567
|
-
// is matched on the raw chunks, before this. Its held tail is flushed by
|
|
568
|
-
// the idle timer: a server never ends on the way to a flush.
|
|
569
|
-
const serverSecrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
570
|
-
const serverOut = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStdout(node, t))
|
|
571
|
-
const serverErr = serverSecrets && maskedEmitter(serverSecrets, (t) => log.taskStderr(node, t))
|
|
572
|
-
// A server writes until the run stops it: its window stays open.
|
|
573
|
-
args.commandWindows?.open(node.id, node.projectDir, commandWriteReach(node, args.workspaceRoot))
|
|
574
|
-
const persistentOpts: Parameters<typeof runPersistent>[0] = {
|
|
575
|
-
command,
|
|
576
|
-
cwd: node.projectDir,
|
|
577
|
-
env,
|
|
578
|
-
onStdout: serverOut ? (chunk) => serverOut.push(chunk) : (chunk) => log.taskStdout(node, chunk),
|
|
579
|
-
onStderr: serverErr ? (chunk) => serverErr.push(chunk) : (chunk) => log.taskStderr(node, chunk),
|
|
580
|
-
...(args.liveChildren !== undefined ? { liveChildren: args.liveChildren } : {}),
|
|
581
|
-
...(args.track !== undefined
|
|
582
|
-
? { onSpawn: (pid: number) => void args.track!(node.id, pid) }
|
|
583
|
-
: {}),
|
|
584
|
-
...(signalChannel ? { signalChannel } : {}),
|
|
585
|
-
...(args.terminal === true ? { terminal: true } : {}),
|
|
586
|
-
}
|
|
587
|
-
if (step.persistent.readyWhen !== undefined) {
|
|
588
|
-
persistentOpts.readyWhen = step.persistent.readyWhen
|
|
589
|
-
}
|
|
590
|
-
// For a persistent task the timeout bounds the readiness wait. Per-task
|
|
591
|
-
// `exec.timeout` wins; else the run-level default (env/workspace/--timeout).
|
|
592
|
-
const effectiveTimeout = taskTimeoutMs(step) ?? args.timeout
|
|
593
|
-
if (effectiveTimeout !== undefined) {
|
|
594
|
-
persistentOpts.timeoutMs = effectiveTimeout
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// A stop that landed during the awaits above (the key, the sandbox's
|
|
598
|
-
// arming, request and wrap) leaves nothing to kill yet: spawned now, the
|
|
599
|
-
// server came up after the teardown and held the run's exit 7 s.
|
|
600
|
-
if (args.stopSignal?.aborted === true) {
|
|
601
|
-
if (bridgeTag !== undefined) releaseBridges(bridgeTag)
|
|
602
|
-
await placeholderSweeper(placeholders)()
|
|
603
|
-
return {
|
|
604
|
-
node,
|
|
605
|
-
status: 'aborted',
|
|
606
|
-
// The signal a spawned server would have been sent (a hang-up forwards SIGTERM).
|
|
607
|
-
exitCode: signalExitCode(forwardedSignal(args.stopSignal.reason)),
|
|
608
|
-
durationMs: 0,
|
|
609
|
-
wallclockStartNs,
|
|
610
|
-
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
const spawn = runPersistent(persistentOpts)
|
|
614
|
-
// The host side of a port bridge lives exactly as long as the server:
|
|
615
|
-
// released on the child's exit, whether the run tore it down or it died.
|
|
616
|
-
// A placeholder the server never wrote goes back the same way.
|
|
617
|
-
// ONE sweep, shared by the exit handler and the readiness failure below
|
|
618
|
-
// — whichever asks second must get the SAME list. A second sweep cannot
|
|
619
|
-
// produce it: the first one's `rm` already happened, so the file is gone
|
|
620
|
-
// and `sweepPlaceholders` skips it. Collecting both lists and unioning
|
|
621
|
-
// them does not close that, because the union only covers an exit
|
|
622
|
-
// handler that FINISHED; one still between its `rm` and its return has
|
|
623
|
-
// published nothing yet, and the readiness path then reports no
|
|
624
|
-
// placeholder at all. The failure then says only "File exists", which is
|
|
625
|
-
// the message the hint exists to explain (seen under the gate's parallel
|
|
626
|
-
// load, and reproduced deterministically by delaying each side, item 450).
|
|
627
|
-
const sweptUntouched = placeholderSweeper(placeholders)
|
|
628
|
-
if (bridgeTag !== undefined || placeholders.length > 0) {
|
|
629
|
-
const tag = bridgeTag
|
|
630
|
-
const onExit = async (): Promise<void> => {
|
|
631
|
-
if (tag !== undefined) releaseBridges(tag)
|
|
632
|
-
await sweptUntouched()
|
|
633
|
-
}
|
|
634
|
-
// A spawn that failed has no child to wait on: release now, or the
|
|
635
|
-
// tag would hold the sandbox's reset for the rest of the process.
|
|
636
|
-
if (spawn.child === undefined) void onExit()
|
|
637
|
-
else void spawn.child.exited.then(onExit, onExit)
|
|
638
|
-
}
|
|
639
|
-
// Said once if readiness is slow: a dependency's output is hidden unless
|
|
640
|
-
// it fails, and with no `exec.timeout` the wait never ends, so a run
|
|
641
|
-
// whose `readyWhen` never matched showed nothing at all.
|
|
642
|
-
const readyWhen = step.persistent.readyWhen
|
|
643
|
-
// How long a server may take to match `readyWhen` before vx says it waits.
|
|
644
|
-
const noticeMs = args.readyNotice ?? 10_000
|
|
645
|
-
const notice =
|
|
646
|
-
readyWhen === undefined
|
|
647
|
-
? undefined
|
|
648
|
-
: setTimeout(() => {
|
|
649
|
-
if (isAborted(args.stopSignal)) return
|
|
650
|
-
const after = noticeMs < 1000 ? `${noticeMs} ms` : `${noticeMs / 1000} s`
|
|
651
|
-
const unbounded = effectiveTimeout === undefined ? ', with no exec.timeout' : ''
|
|
652
|
-
log.status(
|
|
653
|
-
`vx: ${node.id} not ready after ${after}: waiting for a line matching /${serverSecrets?.mask(readyWhen) ?? readyWhen}/ (readyWhen)${unbounded}`,
|
|
654
|
-
)
|
|
655
|
-
}, noticeMs)
|
|
656
|
-
if (notice !== undefined) {
|
|
657
|
-
const quiet = (): void => clearTimeout(notice)
|
|
658
|
-
void spawn.ready.then(quiet, quiet)
|
|
659
|
-
}
|
|
660
|
-
try {
|
|
661
|
-
await spawn.ready
|
|
662
|
-
} catch (err) {
|
|
663
|
-
// A shell that exited before ready can leave its group running
|
|
664
|
-
// (`server & …; exit`): never registered, so no teardown reached it,
|
|
665
|
-
// and it held its port against the next `vx watch` cycle's server.
|
|
666
|
-
if (err instanceof PersistentReadyError && err.reason === 'exited')
|
|
667
|
-
await terminateChildren(() => [spawn.child])
|
|
668
|
-
// A server the run's stop killed while it started is aborted, as any
|
|
669
|
-
// task the stop kills (item 962): it read `failed (never ready:
|
|
670
|
-
// exited, exit 130)` with a recap after every Ctrl-C (C-62). The stop
|
|
671
|
-
// aborts before it kills, so it is set by the time the child is gone.
|
|
672
|
-
// Read through a call: the early return above narrows `aborted` to
|
|
673
|
-
// false, but the stop can land during `spawn.ready`.
|
|
674
|
-
if (isAborted(args.stopSignal)) {
|
|
675
|
-
return {
|
|
676
|
-
node,
|
|
677
|
-
status: 'aborted',
|
|
678
|
-
exitCode: err instanceof PersistentReadyError ? (err.exitCode ?? 1) : 1,
|
|
679
|
-
durationMs: spawn.readyMs(),
|
|
680
|
-
wallclockStartNs,
|
|
681
|
-
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
685
|
-
// The task's OWN stream, not the process's: the frame is where a
|
|
686
|
-
// reader looks for why a task failed, and a run with a custom logger
|
|
687
|
-
// (an embedder) never saw a bare stderr write at all.
|
|
688
|
-
log.taskStderr(node, `\n[vx] ${node.id}: persistent task failed to become ready: ${message}\n`)
|
|
689
|
-
// A server is never traced, so the sandbox's refusals reach no report:
|
|
690
|
-
// a dev server that died on a file outside its grants read only as the
|
|
691
|
-
// tool's own "not found" (2026-10-03). Said for a sandboxed server that
|
|
692
|
-
// exited failing, which is the shape a refusal takes.
|
|
693
|
-
if (
|
|
694
|
-
step.sandbox !== undefined &&
|
|
695
|
-
err instanceof PersistentReadyError &&
|
|
696
|
-
err.reason === 'exited' &&
|
|
697
|
-
err.exitCode !== 0
|
|
698
|
-
) {
|
|
699
|
-
log.taskStderr(
|
|
700
|
-
node,
|
|
701
|
-
`[vx] ${node.id} ran in the sandbox, which reports nothing for a server: a path outside ` +
|
|
702
|
-
`its grants reads as missing (ENOENT), a refused write as read-only (EROFS). Check ` +
|
|
703
|
-
`exec.sandbox.allow, or run the command as a one-shot sandboxed task to see what it was refused.\n`,
|
|
704
|
-
)
|
|
705
|
-
}
|
|
706
|
-
// The server is dead or being torn down, so the sweep on its exit may
|
|
707
|
-
// already be running: ask the shared one rather than starting a second.
|
|
708
|
-
for (const p of await sweptUntouched()) {
|
|
709
|
-
log.taskStderr(node, `${untouchedPlaceholderLine(node.projectDir, p)}\n`)
|
|
710
|
-
}
|
|
711
|
-
// The reason rides the outcome (every label reads it), and a child that
|
|
712
|
-
// exited before ready keeps its own exit code rather than a made-up 1.
|
|
713
|
-
// One the readiness timeout is killing reports the signal's, as an
|
|
714
|
-
// ordinary timeout does (X-24).
|
|
715
|
-
// A readiness timeout rejects once the group is gone (runner.ts), so a
|
|
716
|
-
// server that ignored the TERM holds no port into the next `vx watch`
|
|
717
|
-
// cycle, and a second signal here would cut a one-shot handler short.
|
|
718
|
-
const ready = err instanceof PersistentReadyError ? err : undefined
|
|
719
|
-
return {
|
|
720
|
-
node,
|
|
721
|
-
status: 'failed',
|
|
722
|
-
// A server that traps the TERM and exits 0 still timed out, as a
|
|
723
|
-
// one-shot task that does so reports 143 (X-116).
|
|
724
|
-
exitCode:
|
|
725
|
-
ready?.reason === 'timeout'
|
|
726
|
-
? (await spawn.child.exited) || signalExitCode('SIGTERM')
|
|
727
|
-
: (ready?.exitCode ?? 1),
|
|
728
|
-
durationMs: spawn.readyMs(),
|
|
729
|
-
...(ready !== undefined ? { notReady: ready.reason } : {}),
|
|
730
|
-
wallclockStartNs,
|
|
731
|
-
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Ready only once the stop had landed: a trap that prints the marker on
|
|
736
|
-
// the way down, or a race with the signal. The stop is killing it (it
|
|
737
|
-
// is in `liveChildren`), and it served nobody, as a one-shot that exits
|
|
738
|
-
// 0 on the stop is aborted (item 962).
|
|
739
|
-
if (isAborted(args.stopSignal)) {
|
|
740
|
-
return {
|
|
741
|
-
node,
|
|
742
|
-
status: 'aborted',
|
|
743
|
-
exitCode: signalExitCode(forwardedSignal(args.stopSignal!.reason)),
|
|
744
|
-
durationMs: spawn.readyMs(),
|
|
745
|
-
wallclockStartNs,
|
|
746
|
-
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
args.persistentRegistry?.set(node.id, spawn.child)
|
|
750
|
-
forgetUndeclaredWrites(args, undeclaredWriteReach(node, args.workspaceRoot))
|
|
751
|
-
if (mayWriteFingerprint(node, args.workspaceRoot, claimedFiles(args))) {
|
|
752
|
-
args.fingerprintWatch?.wrote()
|
|
753
|
-
}
|
|
754
|
-
return {
|
|
755
|
-
node,
|
|
756
|
-
status: 'success',
|
|
757
|
-
exitCode: 0,
|
|
758
|
-
durationMs: spawn.readyMs(),
|
|
759
|
-
...(hash !== undefined ? { hash } : {}),
|
|
760
|
-
wallclockStartNs,
|
|
761
|
-
wallclockEndNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
471
|
/**
|
|
766
472
|
* Cached task: the common case. Hash inputs, try cache.get; on hit,
|
|
767
473
|
* clean+restore+replay logs; on miss (or --no-cache), clean outputs,
|
|
@@ -1111,7 +817,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1111
817
|
...(cacheCfg?.inputs.workspaceFiles ?? []),
|
|
1112
818
|
]
|
|
1113
819
|
if (inputs !== undefined && declaredInputs.length > 0 && inputs.files.length === 0) {
|
|
1114
|
-
log
|
|
820
|
+
warnOf(log)(
|
|
1115
821
|
`[vx] ${node.id}: cache.inputs matched no files (${declaredInputs.join(', ')}) — ` +
|
|
1116
822
|
`the key will not change when this project's source does`,
|
|
1117
823
|
)
|
|
@@ -1235,7 +941,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1235
941
|
if (remoteOnly) {
|
|
1236
942
|
raw = new UserError(`${why}, and remote: 'only' keeps it off this machine`)
|
|
1237
943
|
} else {
|
|
1238
|
-
log
|
|
944
|
+
warnOf(log)(`[vx] ${node.id}: ${why} — running it here`)
|
|
1239
945
|
const local = await localExecutor().execute(await buildRequest(true))
|
|
1240
946
|
assertExecuteResult('local', node.id, local)
|
|
1241
947
|
return local
|
|
@@ -1709,13 +1415,13 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1709
1415
|
? ''
|
|
1710
1416
|
: '; if the task writes it, declare it in cache.outputs'
|
|
1711
1417
|
if (stability.severity === 'error') {
|
|
1712
|
-
log
|
|
1418
|
+
warnOf(log)(
|
|
1713
1419
|
`[vx] ${node.id}: ${what} — the result is not saved, ` +
|
|
1714
1420
|
`and inputStability.severity is 'error', so the task fails${hint}`,
|
|
1715
1421
|
)
|
|
1716
1422
|
movedFails = moved === null ? '' : printable(relPosix(args.workspaceRoot, moved))
|
|
1717
1423
|
} else {
|
|
1718
|
-
log
|
|
1424
|
+
warnOf(log)(
|
|
1719
1425
|
`[vx] ${node.id}: ${what} — the result stands, ` +
|
|
1720
1426
|
`but is not saved under a key that no longer describes it${hint}`,
|
|
1721
1427
|
)
|
|
@@ -1728,7 +1434,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1728
1434
|
if (movedFails !== undefined) effectiveExitCode = 1
|
|
1729
1435
|
|
|
1730
1436
|
if (unkeyedUpstream !== undefined && willWrite && effectiveExitCode === 0) {
|
|
1731
|
-
log
|
|
1437
|
+
warnOf(log)(
|
|
1732
1438
|
`[vx] ${node.id}: ran over ${unkeyedUpstream.node.id}'s outputs, which its key no longer ` +
|
|
1733
1439
|
`describes — the result stands, but is not saved`,
|
|
1734
1440
|
)
|
|
@@ -1785,74 +1491,3 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1785
1491
|
: {}),
|
|
1786
1492
|
}
|
|
1787
1493
|
}
|
|
1788
|
-
|
|
1789
|
-
/**
|
|
1790
|
-
* Drop the run's facts about files a task may have written without
|
|
1791
|
-
* declaring them (`undeclaredWriteReach`): the project's git snapshot and
|
|
1792
|
-
* its index OIDs. The next reader
|
|
1793
|
-
* re-enumerates the project (one `git ls-files`) and hashes its files by
|
|
1794
|
-
* content; the per-declaration file-list memos are keyed on the snapshot
|
|
1795
|
-
* array, so they miss with it (item 743).
|
|
1796
|
-
*/
|
|
1797
|
-
function forgetUndeclaredWrites(args: ExecuteArgs, reach: 'none' | 'project' | 'workspace'): void {
|
|
1798
|
-
if (reach === 'none') return
|
|
1799
|
-
if (reach === 'workspace') {
|
|
1800
|
-
args.gitFilesCache?.clear()
|
|
1801
|
-
return
|
|
1802
|
-
}
|
|
1803
|
-
args.gitFilesCache?.delete(args.node.projectDir)
|
|
1804
|
-
args.gitFilesCache?.invalidateWorkspacePartition()
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
/** The two bin directories a task's PATH starts with: its own, then the root's (once when they coincide). */
|
|
1808
|
-
function taskBinDirs(node: TaskNode, workspaceRoot: string): string[] {
|
|
1809
|
-
const bins = [path.join(node.projectDir, 'node_modules', '.bin')]
|
|
1810
|
-
const rootBin = path.join(workspaceRoot, 'node_modules', '.bin')
|
|
1811
|
-
// Identical when the root is itself a project — dedupe rather than list it
|
|
1812
|
-
// twice, so PATH reads the same either way.
|
|
1813
|
-
if (rootBin !== bins[0]) bins.push(rootBin)
|
|
1814
|
-
return bins
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
/**
|
|
1818
|
-
* Build the child-process env for one task. Same arguments at every call site
|
|
1819
|
-
* (persistent + cached). Two `node_modules/.bin` directories are prepended to
|
|
1820
|
-
* PATH: the project's own, then the WORKSPACE ROOT's. Never a sibling
|
|
1821
|
-
* project's, and never an arbitrary ancestor — that is the project-isolation
|
|
1822
|
-
* rule, and the root is not a sibling.
|
|
1823
|
-
*
|
|
1824
|
-
* The root entry is where a monorepo's shared tooling actually lives: declare
|
|
1825
|
-
* `oxlint` once as a root devDependency and every member's `.bin` is empty of
|
|
1826
|
-
* it. Without the root on PATH such a task exits 127, which is what this
|
|
1827
|
-
* repo's own gate did the moment core stopped BEING the root and the two
|
|
1828
|
-
* paths stopped coinciding. It also removes a divergence that mattered more:
|
|
1829
|
-
* the REAPI executor already rebuilds both entries in the action's command,
|
|
1830
|
-
* so a task that resolved on a worker failed on the machine that submitted
|
|
1831
|
-
* it. npm/pnpm/yarn all put the ancestor chain on PATH for the same reason.
|
|
1832
|
-
*/
|
|
1833
|
-
function taskEnv(node: TaskNode, step: ExecConfig, workspaceRoot: string): NodeJS.ProcessEnv {
|
|
1834
|
-
const bins = taskBinDirs(node, workspaceRoot)
|
|
1835
|
-
const env = buildIsolatedEnv({
|
|
1836
|
-
passThrough: step.env?.passThrough ?? [],
|
|
1837
|
-
define: step.env?.define ?? {},
|
|
1838
|
-
source: process.env,
|
|
1839
|
-
binPaths: bins,
|
|
1840
|
-
})
|
|
1841
|
-
env[VX_RUN_WORKSPACE_ENV] = workspaceRoot
|
|
1842
|
-
env[VX_RUN_TASK_ENV] = node.id
|
|
1843
|
-
// A task's own passThrough or define wins.
|
|
1844
|
-
if (env[PM_EXEC_ENV] === undefined) {
|
|
1845
|
-
const manager = packageManagerPath(workspaceRoot)
|
|
1846
|
-
if (manager !== null) env[PM_EXEC_ENV] = manager
|
|
1847
|
-
}
|
|
1848
|
-
return env
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
/** A task's own `exec.timeout` in ms; the schema has checked its form. */
|
|
1852
|
-
function taskTimeoutMs(step: ExecConfig): number | undefined {
|
|
1853
|
-
return step.timeout === undefined ? undefined : parseDuration(step.timeout)!
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
function isAborted(signal: AbortSignal | undefined): boolean {
|
|
1857
|
-
return signal?.aborted === true
|
|
1858
|
-
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFileSync, statSync } from 'node:fs'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import { FILE_HASH_RACY_MS, racyWindowMs } from '../cache/index.js'
|
|
15
|
-
import type
|
|
15
|
+
import { type Logger, warnOf } from '../report/index.js'
|
|
16
16
|
|
|
17
17
|
export class FingerprintWatch {
|
|
18
18
|
private pending = false
|
|
@@ -83,7 +83,7 @@ export class FingerprintWatch {
|
|
|
83
83
|
say(log: Logger): void {
|
|
84
84
|
if (this.said || this.movedFiles === undefined) return
|
|
85
85
|
this.said = true
|
|
86
|
-
log
|
|
86
|
+
warnOf(log)(
|
|
87
87
|
`[vx] a task rewrote \`${this.movedFiles.join('`, `')}\` during the run, which a plugin ` +
|
|
88
88
|
`folds into every key: nothing keyed before it is restored or saved from here on`,
|
|
89
89
|
)
|
|
@@ -36,43 +36,7 @@ export { exitSignal, shellQuote } from '../exec/index.js'
|
|
|
36
36
|
export { forwardedSignal } from './signals.js'
|
|
37
37
|
// `vx cache clean` waits for a run on the workspace before it evicts.
|
|
38
38
|
export { acquireRunLock } from './run-lock.js'
|
|
39
|
-
export { defaultLogger, resolveOutputView } from './logger.js'
|
|
40
|
-
export type { Logger, OutputView } from './logger.js'
|
|
41
39
|
export type { RunPlan, PlannedTask, PlanPrediction, CacheStatus } from './plan.js'
|
|
42
|
-
export {
|
|
43
|
-
createEventBus,
|
|
44
|
-
wireForwarder,
|
|
45
|
-
toWireEvent,
|
|
46
|
-
projectNode,
|
|
47
|
-
projectOutcome,
|
|
48
|
-
outcomeWord,
|
|
49
|
-
} from './events.js'
|
|
50
|
-
export type {
|
|
51
|
-
EventBus,
|
|
52
|
-
RunEvent,
|
|
53
|
-
RunEventSubscriber,
|
|
54
|
-
RunStartInfo,
|
|
55
|
-
WireEvent,
|
|
56
|
-
TaskView,
|
|
57
|
-
OutcomeView,
|
|
58
|
-
} from './events.js'
|
|
59
|
-
export { escapeMarkdownCell, formatRunReportMarkdown, type RunResult } from './run-report.js'
|
|
60
|
-
export {
|
|
61
|
-
EmptyHistoryProvider,
|
|
62
|
-
type HistoryProvider,
|
|
63
|
-
type HistoryTable,
|
|
64
|
-
LocalHistoryProvider,
|
|
65
|
-
type TaskHistory,
|
|
66
|
-
type TaskResources,
|
|
67
|
-
} from './history.js'
|
|
68
|
-
export {
|
|
69
|
-
detectFlaky,
|
|
70
|
-
type FailureMode,
|
|
71
|
-
type FlakyCandidate,
|
|
72
|
-
type FlakyFinding,
|
|
73
|
-
type FlakyTask,
|
|
74
|
-
flakyTasks,
|
|
75
|
-
} from './failure-mode.js'
|
|
76
40
|
export {
|
|
77
41
|
installPlugins,
|
|
78
42
|
type CacheContext,
|
|
@@ -133,17 +97,8 @@ export { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
|
|
|
133
97
|
// module header for why one implementation, not one per sink.
|
|
134
98
|
export { LOG_WIRE_VERSION, TaskLogBuffer } from './task-log-buffer.js'
|
|
135
99
|
export type { TaskLogBundle, TaskLogEntry } from './task-log-buffer.js'
|
|
136
|
-
export {
|
|
137
|
-
assembleRunSummary,
|
|
138
|
-
createTelemetrySource,
|
|
139
|
-
deriveCacheSource,
|
|
140
|
-
isCacheHit,
|
|
141
|
-
isPassStatus,
|
|
142
|
-
TASK_STATUSES,
|
|
143
|
-
TELEMETRY_SCHEMA_VERSION,
|
|
144
|
-
} from './telemetry.js'
|
|
100
|
+
export { assembleRunSummary, createTelemetrySource, TELEMETRY_SCHEMA_VERSION } from './telemetry.js'
|
|
145
101
|
export type {
|
|
146
|
-
CacheSource,
|
|
147
102
|
RunContextRecord,
|
|
148
103
|
RunSummaryRecord,
|
|
149
104
|
TaskTelemetry,
|
|
@@ -163,18 +118,6 @@ export {
|
|
|
163
118
|
detectCi,
|
|
164
119
|
} from './run-context.js'
|
|
165
120
|
export type { CiContext, GitContext, HostContext, WorkspaceIdentity } from './run-context.js'
|
|
166
|
-
export {
|
|
167
|
-
cacheKeyDiff,
|
|
168
|
-
explainCacheKey as explainCacheKeyQuery,
|
|
169
|
-
getInvocation,
|
|
170
|
-
getRun,
|
|
171
|
-
latestRunId,
|
|
172
|
-
listInvocations,
|
|
173
|
-
listRuns,
|
|
174
|
-
inputWriters,
|
|
175
|
-
rootCauses,
|
|
176
|
-
whyDidThisRerun as whyDidThisRerunQuery,
|
|
177
|
-
} from './metrics.js'
|
|
178
121
|
export {
|
|
179
122
|
runFailures,
|
|
180
123
|
type RunFailures,
|
|
@@ -182,20 +125,5 @@ export {
|
|
|
182
125
|
type TaskLog,
|
|
183
126
|
taskLog,
|
|
184
127
|
} from './run-failures.js'
|
|
185
|
-
export type { OutputLocation } from './path-links.js'
|
|
186
128
|
export { resolveRunId, shortRunId } from './run-id.js'
|
|
187
|
-
export type {
|
|
188
|
-
CacheEntryRow,
|
|
189
|
-
CacheKeyDiff,
|
|
190
|
-
InputWriter,
|
|
191
|
-
RootCause,
|
|
192
|
-
CacheKeyExplanation,
|
|
193
|
-
InputDiffEntry,
|
|
194
|
-
InvocationDetail,
|
|
195
|
-
ListInvocationsArgs,
|
|
196
|
-
ListRunsArgs,
|
|
197
|
-
RunDetail,
|
|
198
|
-
RunSummaryRow,
|
|
199
|
-
WhyDidThisRerun,
|
|
200
|
-
} from './metrics.js'
|
|
201
129
|
export { collectInfo, type CollectInfoOptions, type InfoFacts } from './doctor.js'
|