@vzn/vx 0.0.646 → 0.0.648
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 +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
1
2
|
import type { ExecConfig, TaskConfig, CacheConfig, InputStability } from '../config.js'
|
|
2
3
|
import {
|
|
3
4
|
ArtifactVanishedError,
|
|
@@ -43,7 +44,7 @@ import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '..
|
|
|
43
44
|
import {
|
|
44
45
|
killGraceMs,
|
|
45
46
|
MASKED,
|
|
46
|
-
|
|
47
|
+
maskedStreams,
|
|
47
48
|
outputMatcher,
|
|
48
49
|
printable,
|
|
49
50
|
relPosix,
|
|
@@ -67,9 +68,15 @@ import {
|
|
|
67
68
|
type WithheldLink,
|
|
68
69
|
withheldLinkLine,
|
|
69
70
|
} from './sandbox-request.js'
|
|
70
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
bracketLiteralHint,
|
|
73
|
+
markUnsaved,
|
|
74
|
+
saveMiss,
|
|
75
|
+
type OutputDirSnapshot,
|
|
76
|
+
type SaveFacts,
|
|
77
|
+
} from './miss-save.js'
|
|
71
78
|
import type { FingerprintWatch } from './fingerprint-watch.js'
|
|
72
|
-
import { restoreHit } from './hit-restore.js'
|
|
79
|
+
import { currentHit, restoreHit } from './hit-restore.js'
|
|
73
80
|
import type { MissExplainer } from '../history/index.js'
|
|
74
81
|
import { shellVerdict } from './shell-verdict.js'
|
|
75
82
|
import { sandboxFix } from './sandbox-fix.js'
|
|
@@ -111,6 +118,8 @@ export interface ExecuteArgs {
|
|
|
111
118
|
log: Logger
|
|
112
119
|
/** The executor this task was PLACED on (run.ts, before scheduling). */
|
|
113
120
|
executor: TaskExecutor
|
|
121
|
+
/** Told the task executes, so the executor's demand counts it again (`ExecutorDemand.executing`). */
|
|
122
|
+
executing?: ((id: string) => void) | undefined
|
|
114
123
|
/**
|
|
115
124
|
* `exec.remote: 'only'` with no remote executor to take it: succeed
|
|
116
125
|
* WITHOUT executing, cleaning, restoring or saving anything on this
|
|
@@ -276,6 +285,8 @@ export interface ExecuteArgs {
|
|
|
276
285
|
carried?: true
|
|
277
286
|
/** The persistent tasks its `servers` executor starts where it runs it. */
|
|
278
287
|
servers?: readonly TaskNode[]
|
|
288
|
+
/** Each of `servers`' nested project dirs, in its order. */
|
|
289
|
+
serversNested?: readonly (readonly string[])[]
|
|
279
290
|
/** When each command of the run ran and where it may write (command-windows.ts). */
|
|
280
291
|
commandWindows?: CommandWindows
|
|
281
292
|
}
|
|
@@ -306,6 +317,28 @@ export async function executeTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
306
317
|
return executeCachedTask(args)
|
|
307
318
|
}
|
|
308
319
|
|
|
320
|
+
/**
|
|
321
|
+
* A probed hit `executeTask` would restore, settled now when its tree is
|
|
322
|
+
* already current (`currentHit`): the thunk replays its output and returns
|
|
323
|
+
* the outcome. Undefined dispatches it as usual. A dispatch, a slot and the
|
|
324
|
+
* promises around it were ~10 µs of each of a warm run's hits.
|
|
325
|
+
*/
|
|
326
|
+
export function settleProbedHit(args: ExecuteArgs): (() => TaskOutcome) | undefined {
|
|
327
|
+
const probed = args.preProbed
|
|
328
|
+
if (probed?.hit == null || !readsCache(args) || args.remoteOnlyNoop === true) return undefined
|
|
329
|
+
if (isGroupTask(args.node) || args.node.config.exec?.persistent !== undefined) return undefined
|
|
330
|
+
// A moved fingerprint is said, and demotes the hit, on the dispatch.
|
|
331
|
+
if (args.fingerprintWatch?.moved() !== undefined) return undefined
|
|
332
|
+
const restore = {
|
|
333
|
+
args,
|
|
334
|
+
hash: probed.hash,
|
|
335
|
+
hit: probed.hit,
|
|
336
|
+
cacheOpStart: performance.now(),
|
|
337
|
+
taskStartNs: process.hrtime.bigint() - args.runStartHrTimeNs,
|
|
338
|
+
}
|
|
339
|
+
return currentHit(restore)
|
|
340
|
+
}
|
|
341
|
+
|
|
309
342
|
/** Whether this task reads the cache: a cached, not remote-only task with a read axis on. */
|
|
310
343
|
function readsCache(args: ExecuteArgs): boolean {
|
|
311
344
|
const policy = args.cachePolicy ?? FULL_CACHE_POLICY
|
|
@@ -364,7 +397,7 @@ async function restoreOrMiss(
|
|
|
364
397
|
/**
|
|
365
398
|
* What a write to an input during the command means, each field highest
|
|
366
399
|
* first: the env var (`layers.run`), the task's `cache.inputStability`,
|
|
367
|
-
* the workspace's, then `'immutable'` and `'warn'` (
|
|
400
|
+
* the workspace's, then `'immutable'` and `'warn'` (2026-10-12), so
|
|
368
401
|
* by default any such write withholds the save, on CI too, since a
|
|
369
402
|
* concurrent task can change an input and put it back.
|
|
370
403
|
*/
|
|
@@ -549,7 +582,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
549
582
|
// The local executor keeps its own timer from the spawn (it signals the
|
|
550
583
|
// process group), so its request carries no clock: armed here, it also
|
|
551
584
|
// timed the sandbox's wrap, and a wrap slower than the timeout aborted
|
|
552
|
-
// the request before the spawn — a task reported killed, never run.
|
|
585
|
+
// the request before the spawn — a task reported killed, never run. So
|
|
586
|
+
// does an executor that declares `timeouts`.
|
|
553
587
|
let timeoutTimer: ReturnType<typeof setTimeout> | undefined
|
|
554
588
|
let timeoutFired = false
|
|
555
589
|
// The attempt's listener on the run's stop signal, taken off once the
|
|
@@ -570,7 +604,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
570
604
|
}
|
|
571
605
|
clearTimeout(timeoutTimer)
|
|
572
606
|
timeoutFired = false
|
|
573
|
-
if (effectiveTimeout !== undefined && !local) {
|
|
607
|
+
if (effectiveTimeout !== undefined && !local && args.executor.timeouts !== true) {
|
|
574
608
|
timeoutTimer = setTimeout(() => {
|
|
575
609
|
timeoutFired = true
|
|
576
610
|
stop.abort(new Error(`timed out after ${effectiveTimeout}ms`))
|
|
@@ -598,7 +632,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
598
632
|
const why = timeoutFired ? `timeout (${effectiveTimeout}ms)` : 'stop'
|
|
599
633
|
log.taskStderr(
|
|
600
634
|
node,
|
|
601
|
-
|
|
635
|
+
`${midLine ? '\n' : ''}vx: ${executorLabel(args.executor)} did not return within ${graceMs}ms of the ${why}; abandoned\n`,
|
|
602
636
|
)
|
|
603
637
|
const reason: unknown = signal.reason
|
|
604
638
|
resolve({
|
|
@@ -702,8 +736,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
702
736
|
let wsStampedBefore: ReadonlyMap<string, OutputStamp> | undefined
|
|
703
737
|
// `cache.outputs.clean: false` leaves the outputs to the first attempt's
|
|
704
738
|
// command, and the save stores whatever they hold after it, a leftover
|
|
705
|
-
// included: the user opted out of the clean-tree guarantee
|
|
706
|
-
// 2026-10-15). A retry still starts clean. An additive task never cleans
|
|
739
|
+
// included: the user opted out of the clean-tree guarantee
|
|
740
|
+
// (2026-10-15). A retry still starts clean. An additive task never cleans
|
|
707
741
|
// by glob anyway.
|
|
708
742
|
const keepOutputs = !additive && (cacheCfg?.outputs.clean ?? args.cleanOutputs ?? true) === false
|
|
709
743
|
|
|
@@ -797,11 +831,13 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
797
831
|
for (const c of captured)
|
|
798
832
|
if (c.kind === 'env' && named.includes(c.name) && !secretNamed(c.name))
|
|
799
833
|
c.hash = MASKED + c.hash
|
|
834
|
+
// The values of secret-named variables, masked in what the task prints
|
|
835
|
+
// and what the cache keeps of it (L-11); null when there are none.
|
|
836
|
+
const secrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
800
837
|
// A runtime probe's row is named by its command, which a TS config may
|
|
801
838
|
// build from `process.env`: the run history stored it and `vx why`
|
|
802
839
|
// printed it (L-11).
|
|
803
|
-
const nameSecrets =
|
|
804
|
-
captured.length > 0 ? secretMask([process.env, env, step.env?.define], step.env?.secret) : null
|
|
840
|
+
const nameSecrets = captured.length > 0 ? secrets : null
|
|
805
841
|
if (nameSecrets !== null) {
|
|
806
842
|
// Two probes that differ only by a secret mask to one name, and a row
|
|
807
843
|
// is stored once per name: numbered, the second's change still shows.
|
|
@@ -829,7 +865,11 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
829
865
|
if (inputs !== undefined && declaredInputs.length > 0 && inputs.files.length === 0) {
|
|
830
866
|
warnOf(log)(
|
|
831
867
|
`[vx] ${node.id}: inputs matched no files (${declaredInputs.join(', ')}) — ` +
|
|
832
|
-
`the key will not change when this project's source does
|
|
868
|
+
`the key will not change when this project's source does` +
|
|
869
|
+
bracketLiteralHint([
|
|
870
|
+
...(cfg.inputs?.files ?? []).map((g) => [node.projectDir, g] as const),
|
|
871
|
+
...(cfg.inputs?.workspaceFiles ?? []).map((g) => [args.workspaceRoot, g] as const),
|
|
872
|
+
]),
|
|
833
873
|
)
|
|
834
874
|
}
|
|
835
875
|
|
|
@@ -902,6 +942,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
902
942
|
const cleanedWsRels = await cleanWorkspaceOutputs(wsCleanArgs)
|
|
903
943
|
args.gitFilesCache?.markWorkspaceOutputsChanged(args.workspaceRoot, cleanedWsRels)
|
|
904
944
|
}
|
|
945
|
+
// What the attempt before this one left, put back if this one is
|
|
946
|
+
// withdrawn: it stands, with its output and its refusals.
|
|
947
|
+
const standing = { output, violations, flushMasked }
|
|
905
948
|
violations = []
|
|
906
949
|
const endReq = span('miss: build request')
|
|
907
950
|
const req = await buildRequest(isLocalExecutor(args.executor))
|
|
@@ -910,6 +953,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
910
953
|
// cleans or builds its request, after the loop's own check: the retry
|
|
911
954
|
// is withdrawn and the attempt before it stands.
|
|
912
955
|
if (attempt > 1 && args.failFast?.aborted === true) {
|
|
956
|
+
output = standing.output
|
|
957
|
+
violations = standing.violations
|
|
958
|
+
flushMasked = standing.flushMasked
|
|
913
959
|
clearTimeout(timeoutTimer)
|
|
914
960
|
unlistenStop?.()
|
|
915
961
|
unlistenStop = undefined
|
|
@@ -931,6 +977,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
931
977
|
node.projectDir,
|
|
932
978
|
commandWriteReach(node, args.workspaceRoot),
|
|
933
979
|
) ?? (() => {})
|
|
980
|
+
args.executing?.(node.id)
|
|
934
981
|
try {
|
|
935
982
|
running = Promise.resolve(args.executor.execute(req))
|
|
936
983
|
} catch (thrown) {
|
|
@@ -950,8 +997,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
950
997
|
const why = secrets?.mask(raw.message) ?? raw.message
|
|
951
998
|
if (remoteOnly) {
|
|
952
999
|
raw = new UserError(`${why}, and remote: 'only' keeps it off this machine`)
|
|
1000
|
+
} else if (args.executor.exclusive === true) {
|
|
1001
|
+
raw = new UserError(`${why}, and ${args.executor.name} runs no task on this machine`)
|
|
953
1002
|
} else {
|
|
954
|
-
warnOf(log)(
|
|
1003
|
+
warnOf(log)(
|
|
1004
|
+
`[vx] ${node.id}: ${executorLabel(args.executor)} gave it back: ${why} — running it here`,
|
|
1005
|
+
)
|
|
955
1006
|
const local = await localExecutor().execute(await buildRequest(true))
|
|
956
1007
|
assertExecuteResult('local', node.id, local)
|
|
957
1008
|
return local
|
|
@@ -977,6 +1028,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
977
1028
|
load = untrack?.() ?? load
|
|
978
1029
|
untrack = undefined
|
|
979
1030
|
})
|
|
1031
|
+
.catch((thrown: unknown) => {
|
|
1032
|
+
// The scheduler prints the throw's line into the task's stream,
|
|
1033
|
+
// after the masked tail the finally above flushed.
|
|
1034
|
+
if (midLine) log.taskStderr(node, '\n')
|
|
1035
|
+
throw thrown
|
|
1036
|
+
})
|
|
980
1037
|
endExec()
|
|
981
1038
|
if (secrets !== null)
|
|
982
1039
|
res = {
|
|
@@ -1016,7 +1073,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1016
1073
|
// before the strace pass shipped, false since, and the branch below is
|
|
1017
1074
|
// live on Linux.) Violations surface via `TaskOutcome.sandboxViolationLines`.
|
|
1018
1075
|
let code = res.exitCode
|
|
1019
|
-
// ANY violation fails the task (
|
|
1076
|
+
// ANY violation fails the task (2026-09-05). A sandboxed task
|
|
1020
1077
|
// declares what it touches; a denial means it touched something else,
|
|
1021
1078
|
// and an artifact built while reading — or failing to read — something
|
|
1022
1079
|
// the cache key never folded is not a safe thing to replay. A task that
|
|
@@ -1032,7 +1089,10 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1032
1089
|
// 137 (item 1063).
|
|
1033
1090
|
if (res.timedOut) {
|
|
1034
1091
|
const how = res.signal === 'SIGKILL' ? 'SIGKILL after the SIGTERM grace' : 'SIGTERM'
|
|
1035
|
-
log.taskStderr(
|
|
1092
|
+
log.taskStderr(
|
|
1093
|
+
node,
|
|
1094
|
+
`${midLine ? '\n' : ''}[vx] timed out after ${effectiveTimeout}ms — killed (${how})\n`,
|
|
1095
|
+
)
|
|
1036
1096
|
// Force a non-zero classification even if the child TRAPPED SIGTERM and
|
|
1037
1097
|
// still exited 0 (`trap 'exit 0' TERM`, a common graceful-shutdown
|
|
1038
1098
|
// pattern). Without this a timed-out task is classified `success` and its
|
|
@@ -1047,7 +1107,15 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1047
1107
|
// about what sent it (259). One frame line names the rule. A spawn that
|
|
1048
1108
|
// threw ran no shell: its 127 is vx's, and the line would send the
|
|
1049
1109
|
// reader after a command that exists (A-41). A run's stop killed it
|
|
1050
|
-
// itself: the SIGKILL past the grace read as the OOM killer's.
|
|
1110
|
+
// itself: the SIGKILL past the grace read as the OOM killer's. A
|
|
1111
|
+
// SIGINT/SIGTERM death waits a turn for a pending handler first, as the
|
|
1112
|
+
// shutdown check below does: a Ctrl-C reaches the child and vx together.
|
|
1113
|
+
if (
|
|
1114
|
+
!res.timedOut &&
|
|
1115
|
+
(res.signal === 'SIGINT' || res.signal === 'SIGTERM') &&
|
|
1116
|
+
args.stopSignal?.aborted !== true
|
|
1117
|
+
)
|
|
1118
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
1051
1119
|
if (!res.timedOut && res.spawnFailed !== true && args.stopSignal?.aborted !== true) {
|
|
1052
1120
|
const verdict = shellVerdict({
|
|
1053
1121
|
code,
|
|
@@ -1059,7 +1127,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1059
1127
|
})
|
|
1060
1128
|
// The line quotes the command's first word, which a config may have
|
|
1061
1129
|
// built from a secret: masked as the task's own output is (L-37).
|
|
1062
|
-
if (verdict !== undefined)
|
|
1130
|
+
if (verdict !== undefined)
|
|
1131
|
+
log.taskStderr(node, `${midLine ? '\n' : ''}${secrets?.mask(verdict) ?? verdict}\n`)
|
|
1063
1132
|
}
|
|
1064
1133
|
return { result: res, exitCode: code }
|
|
1065
1134
|
}
|
|
@@ -1075,7 +1144,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1075
1144
|
await args.deferred.materializeFor(node)
|
|
1076
1145
|
} catch (err) {
|
|
1077
1146
|
const msg = err instanceof Error ? err.message : String(err)
|
|
1078
|
-
|
|
1147
|
+
// Nothing ran: the note is the frame's first line.
|
|
1148
|
+
log.taskStderr(node, `[vx] ${msg}\n`)
|
|
1079
1149
|
return {
|
|
1080
1150
|
node,
|
|
1081
1151
|
status: 'failed',
|
|
@@ -1088,14 +1158,19 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1088
1158
|
}
|
|
1089
1159
|
}
|
|
1090
1160
|
|
|
1091
|
-
// The values of secret-named variables, masked in what the task prints
|
|
1092
|
-
// and what the cache keeps of it (L-11); null when there are none.
|
|
1093
|
-
const secrets = secretMask([process.env, env, step.env?.define], step.env?.secret)
|
|
1094
1161
|
let flushMasked = (): void => {}
|
|
1162
|
+
// The attempt's output, both streams, last ended mid-line: vx's own line
|
|
1163
|
+
// (a retry, a timeout, the shell's verdict, an abandoned or thrown
|
|
1164
|
+
// executor) starts its own, and only then: a newline always before it
|
|
1165
|
+
// printed a blank line under the OUTPUT rule or the task's last line. Glued, a frame read
|
|
1166
|
+
// `partialvx: retrying …`.
|
|
1167
|
+
let midLine = false
|
|
1095
1168
|
// The attempt's output as the logger got it (masked): the entry's when
|
|
1096
1169
|
// the task saves, else only what a failure reports (`failedOutput`).
|
|
1097
1170
|
let output: BoundedCapture | undefined
|
|
1098
|
-
|
|
1171
|
+
// Masked again whole: the live stream lets a held piece go after a pause.
|
|
1172
|
+
const storeMask = secrets ?? undefined
|
|
1173
|
+
const storedOutput = (): string => encodeOutputLog(output?.chunks() ?? [], storeMask)
|
|
1099
1174
|
const failedAttempts: { endedAt: number; exitCode: number; timedOut?: true }[] = []
|
|
1100
1175
|
// The sampling of the attempt's process tree, stopped once it settles.
|
|
1101
1176
|
let untrack: (() => TreeLoad | undefined) | undefined
|
|
@@ -1184,7 +1259,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1184
1259
|
})
|
|
1185
1260
|
log.taskStderr(
|
|
1186
1261
|
node,
|
|
1187
|
-
|
|
1262
|
+
`${midLine ? '\n' : ''}vx: retrying ${node.id} (attempt ${attempt + 1}/${maxAttempts}) after ${result.timedOut === true ? 'a timeout' : `exit ${effectiveExitCode}`}\n`,
|
|
1188
1263
|
)
|
|
1189
1264
|
}
|
|
1190
1265
|
|
|
@@ -1194,20 +1269,19 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1194
1269
|
? new BoundedCapture()
|
|
1195
1270
|
: new BoundedCapture(FAILED_OUTPUT_HEAD_CHARS, FAILED_OUTPUT_TAIL_CHARS)
|
|
1196
1271
|
output = kept
|
|
1272
|
+
midLine = false
|
|
1197
1273
|
const toOut = (t: string): void => {
|
|
1198
1274
|
kept.push(t)
|
|
1275
|
+
if (t.length > 0) midLine = !t.endsWith('\n')
|
|
1199
1276
|
log.taskStdout(node, t)
|
|
1200
1277
|
}
|
|
1201
1278
|
const toErr = (t: string): void => {
|
|
1202
1279
|
kept.push(t, true)
|
|
1280
|
+
if (t.length > 0) midLine = !t.endsWith('\n')
|
|
1203
1281
|
log.taskStderr(node, t)
|
|
1204
1282
|
}
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
flushMasked = () => {
|
|
1208
|
-
out?.end()
|
|
1209
|
-
err?.end()
|
|
1210
|
-
}
|
|
1283
|
+
const masked = secrets && maskedStreams(secrets, toOut, toErr)
|
|
1284
|
+
flushMasked = () => masked?.end()
|
|
1211
1285
|
const base: ExecuteRequest = {
|
|
1212
1286
|
taskId: node.id,
|
|
1213
1287
|
workspaceRoot: args.workspaceRoot,
|
|
@@ -1217,8 +1291,8 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1217
1291
|
env,
|
|
1218
1292
|
envDefine: step.env?.define ?? {},
|
|
1219
1293
|
capture,
|
|
1220
|
-
onStdout:
|
|
1221
|
-
onStderr:
|
|
1294
|
+
onStdout: masked ? masked.out : toOut,
|
|
1295
|
+
onStderr: masked ? masked.err : toErr,
|
|
1222
1296
|
...(args.liveChildren !== undefined ? { liveChildren: args.liveChildren } : {}),
|
|
1223
1297
|
...(args.track !== undefined
|
|
1224
1298
|
? {
|
|
@@ -1231,7 +1305,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1231
1305
|
...(effectiveTimeout !== undefined ? { timeoutMs: effectiveTimeout } : {}),
|
|
1232
1306
|
...(inputs !== undefined ? { inputs } : {}),
|
|
1233
1307
|
...(cfgCacheable ? { cacheKey: hash } : {}),
|
|
1308
|
+
...(cfgCacheable && !remoteOnly && !willSave ? { unsaved: true } : {}),
|
|
1234
1309
|
...(remoteOnly ? { remoteOnly: true } : {}),
|
|
1310
|
+
...(node.platform !== undefined ? { platform: node.platform } : {}),
|
|
1235
1311
|
...(deferralRequested ? { download: 'deferred' as const } : {}),
|
|
1236
1312
|
// `--force`/`--no-cache` reach a remote executor's private record
|
|
1237
1313
|
// through this flag — the policy gates above only cover vx's OWN cache.
|
|
@@ -1240,7 +1316,11 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1240
1316
|
...(args.terminal === true ? { terminal: true as const } : {}),
|
|
1241
1317
|
...(args.servers === undefined
|
|
1242
1318
|
? {}
|
|
1243
|
-
: {
|
|
1319
|
+
: {
|
|
1320
|
+
servers: args.servers.map((s, i) =>
|
|
1321
|
+
serverSpec(s, args.workspaceRoot, args.serversNested?.[i] ?? []),
|
|
1322
|
+
),
|
|
1323
|
+
}),
|
|
1244
1324
|
}
|
|
1245
1325
|
// The signal last: it arms a plugin's `exec.timeout`, and the sandbox's arming
|
|
1246
1326
|
// below is vx's work, not the task's. Armed first, a 60 ms timeout
|
|
@@ -1267,7 +1347,11 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1267
1347
|
let movedFails: string | undefined
|
|
1268
1348
|
let own: Awaited<ReturnType<typeof ownOutputs>>
|
|
1269
1349
|
let saved = false
|
|
1270
|
-
if (
|
|
1350
|
+
if (
|
|
1351
|
+
effectiveExitCode === 0 &&
|
|
1352
|
+
willSave &&
|
|
1353
|
+
(deferralRequested || result.outputs?.kind === 'deferred')
|
|
1354
|
+
) {
|
|
1271
1355
|
// The outputs never landed here: no artifact, no rows. A partial local
|
|
1272
1356
|
// record (a row with no artifact) is exactly the corrupt-entry shape
|
|
1273
1357
|
// `restoreOutputs` refuses, so writing none is the only clean answer.
|
|
@@ -1393,6 +1477,12 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1393
1477
|
*/
|
|
1394
1478
|
async function movedSinceKey(): Promise<string | null | undefined> {
|
|
1395
1479
|
if (described!.hash !== hash) return null
|
|
1480
|
+
// Changed where an executor ran it: this disk never shows it.
|
|
1481
|
+
if (result.changed !== undefined && result.changed.length > 0) {
|
|
1482
|
+
const changed = new Set(result.changed.map((f) => path.join(args.workspaceRoot, f)))
|
|
1483
|
+
const there = keyedInputs(described!.facts).find((f) => changed.has(f.path))
|
|
1484
|
+
if (there !== undefined) return there.path
|
|
1485
|
+
}
|
|
1396
1486
|
return (
|
|
1397
1487
|
(await movedInput(
|
|
1398
1488
|
keyedInputs(described!.facts),
|
|
@@ -1483,7 +1573,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1483
1573
|
...(keylessBy !== undefined ? { keylessBy } : {}),
|
|
1484
1574
|
...(saved ? { saved: true as const } : {}),
|
|
1485
1575
|
...(effectiveExitCode !== 0 && output !== undefined
|
|
1486
|
-
? { failedOutput: failedOutputLog(output, willSave || args.keepRun !== undefined) }
|
|
1576
|
+
? { failedOutput: failedOutputLog(output, willSave || args.keepRun !== undefined, storeMask) }
|
|
1487
1577
|
: {}),
|
|
1488
1578
|
...(args.keepRun !== undefined &&
|
|
1489
1579
|
(effectiveExitCode !== 0 ? args.keepRun.failures : !willSave) &&
|
|
@@ -1497,11 +1587,13 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
1497
1587
|
...(load !== undefined ? { peakCores: load.peakCores, avgRssBytes: load.avgRssBytes } : {}),
|
|
1498
1588
|
...(result.where !== undefined ? { where: result.where } : {}),
|
|
1499
1589
|
...(result.where !== undefined && result.machine !== undefined
|
|
1500
|
-
? {
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1590
|
+
? {
|
|
1591
|
+
whereCores: result.machine.cores,
|
|
1592
|
+
whereMemoryBytes: result.machine.memoryBytes,
|
|
1593
|
+
...(result.machine.cpuShare === undefined ? {} : { cpuShare: result.machine.cpuShare }),
|
|
1594
|
+
}
|
|
1504
1595
|
: {}),
|
|
1596
|
+
...(result.outputs?.kind === 'deferred' ? { outputs: 'deferred' as const } : {}),
|
|
1505
1597
|
wallclockStartNs,
|
|
1506
1598
|
wallclockEndNs,
|
|
1507
1599
|
...(finalViolations.length > 0
|
|
@@ -85,7 +85,7 @@ export class FingerprintWatch {
|
|
|
85
85
|
this.said = true
|
|
86
86
|
warnOf(log)(
|
|
87
87
|
`[vx] a task rewrote \`${this.movedFiles.join('`, `')}\` during the run, which a plugin ` +
|
|
88
|
-
`
|
|
88
|
+
`puts into every key: nothing keyed before it is restored or saved from here on`,
|
|
89
89
|
)
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The task graph memo: the graph a run built, read back whole when nothing
|
|
2
|
+
// it is built from has moved. Building 1,206 tasks' graph was 4.5 ms of a
|
|
3
|
+
// warm run (2026-10-23), nearly all of it code a short process runs cold;
|
|
4
|
+
// reading it back is half that.
|
|
5
|
+
//
|
|
6
|
+
// A run's configs come from the lock and nowhere else when no plugin fills
|
|
7
|
+
// the `project` stage, so the token names them by the lock's bytes instead
|
|
8
|
+
// of serializing them (3 ms at 1,206 tasks): the bytes, the projects
|
|
9
|
+
// discovered and loaded, their package edges, the requested tasks, the
|
|
10
|
+
// rules, the root and the code are everything `buildTaskGraph` reads. A
|
|
11
|
+
// scoped load hands undeclared `^name`s back instead of refusing them, so
|
|
12
|
+
// it is never remembered.
|
|
13
|
+
|
|
14
|
+
import type { FoldMemo } from '../cache/index.js'
|
|
15
|
+
import { buildTaskGraph, declaredTask, type TaskNode } from '../graph/index.js'
|
|
16
|
+
import { xxh3hex } from '../util/index.js'
|
|
17
|
+
import type { ProjectMeta } from '../workspace/index.js'
|
|
18
|
+
import { codeIdentity } from './run-keys.js'
|
|
19
|
+
|
|
20
|
+
type BuildGraphOptions = Parameters<typeof buildTaskGraph>[0]
|
|
21
|
+
|
|
22
|
+
/** Moves every remembered graph: bump it when what a token stands for changes. */
|
|
23
|
+
const GRAPH_MEMO_VERSION = 'graph-memo-v1'
|
|
24
|
+
|
|
25
|
+
/** What names a run's configs: the lock they were read from and the projects discovered. */
|
|
26
|
+
export interface GraphMemo {
|
|
27
|
+
folds: FoldMemo
|
|
28
|
+
lock: string
|
|
29
|
+
metas: readonly ProjectMeta[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A node as the memo holds it: its project and task, requested, its deps
|
|
34
|
+
* and the tasks it adds outputs to as positions in the list (ids in full
|
|
35
|
+
* were 123 KB for 1,206 tasks, and parsing them back cost what the walk
|
|
36
|
+
* saved), and the globs that add to its own outputs.
|
|
37
|
+
*/
|
|
38
|
+
type Row = [
|
|
39
|
+
project: string,
|
|
40
|
+
task: string,
|
|
41
|
+
requested: 0 | 1,
|
|
42
|
+
deps: number[],
|
|
43
|
+
adds?: number[] | null,
|
|
44
|
+
added?: string[] | null,
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `buildTaskGraph`, answered from the memo when it built this graph before,
|
|
49
|
+
* and the token that names the graph (undefined when not remembered).
|
|
50
|
+
*/
|
|
51
|
+
export function memoGraph(
|
|
52
|
+
options: BuildGraphOptions,
|
|
53
|
+
memo: GraphMemo | undefined,
|
|
54
|
+
): { nodes: Map<string, TaskNode>; token: string | undefined } {
|
|
55
|
+
if (memo === undefined || options.undeclaredDeps !== undefined) {
|
|
56
|
+
return { nodes: buildTaskGraph(options), token: undefined }
|
|
57
|
+
}
|
|
58
|
+
const { projects, packageGraph } = options
|
|
59
|
+
const parts: string[] = [
|
|
60
|
+
GRAPH_MEMO_VERSION,
|
|
61
|
+
codeIdentity(),
|
|
62
|
+
memo.lock,
|
|
63
|
+
options.workspaceRoot ?? '',
|
|
64
|
+
JSON.stringify(options.rules ?? null),
|
|
65
|
+
String(packageGraph.edgeSource),
|
|
66
|
+
JSON.stringify(options.requested),
|
|
67
|
+
]
|
|
68
|
+
for (const m of memo.metas) {
|
|
69
|
+
parts.push(
|
|
70
|
+
m.name,
|
|
71
|
+
m.dir,
|
|
72
|
+
m.configPath ?? '\u0001',
|
|
73
|
+
packageGraph.directDeps(m.name).join('\u0001'),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
for (const name of projects.keys()) parts.push(name)
|
|
77
|
+
const id = `\0graph\0${xxh3hex(JSON.stringify(options.requested))}`
|
|
78
|
+
const token = xxh3hex(parts.join('\0'))
|
|
79
|
+
const row = memo.folds.get(id, token)
|
|
80
|
+
if (row !== undefined) {
|
|
81
|
+
const nodes = rebuilt(row, projects)
|
|
82
|
+
if (nodes !== undefined) return { nodes, token }
|
|
83
|
+
}
|
|
84
|
+
const nodes = buildTaskGraph(options)
|
|
85
|
+
memo.folds.set(id, token, JSON.stringify(rowsOf(nodes)))
|
|
86
|
+
return { nodes, token }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rowsOf(nodes: Map<string, TaskNode>): Row[] {
|
|
90
|
+
const at = new Map<string, number>()
|
|
91
|
+
for (const id of nodes.keys()) at.set(id, at.size)
|
|
92
|
+
const pos = (ids: readonly string[]): number[] => ids.map((d) => at.get(d)!)
|
|
93
|
+
return [...nodes.values()].map((n) => {
|
|
94
|
+
const row: Row = [n.projectName, n.taskName, n.requested ? 1 : 0, pos(n.deps)]
|
|
95
|
+
if (n.addsToOutputsOf !== undefined || n.outputsAddedToBy !== undefined)
|
|
96
|
+
row.push(
|
|
97
|
+
n.addsToOutputsOf === undefined ? null : pos(n.addsToOutputsOf),
|
|
98
|
+
n.outputsAddedToBy ?? null,
|
|
99
|
+
)
|
|
100
|
+
return row
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function rebuilt(
|
|
105
|
+
row: string,
|
|
106
|
+
projects: BuildGraphOptions['projects'],
|
|
107
|
+
): Map<string, TaskNode> | undefined {
|
|
108
|
+
let rows: Row[]
|
|
109
|
+
try {
|
|
110
|
+
rows = JSON.parse(row)
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
const list: TaskNode[] = []
|
|
115
|
+
for (const [projectName, taskName, requested] of rows) {
|
|
116
|
+
const project = projects.get(projectName)
|
|
117
|
+
const config = declaredTask(project?.config, taskName)
|
|
118
|
+
if (project === undefined || config === undefined) return undefined
|
|
119
|
+
list.push({
|
|
120
|
+
id: `${projectName}#${taskName}`,
|
|
121
|
+
projectName,
|
|
122
|
+
projectDir: project.dir,
|
|
123
|
+
taskName,
|
|
124
|
+
config,
|
|
125
|
+
deps: [],
|
|
126
|
+
requested: requested === 1,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
const ids = (at: readonly number[]): string[] => at.map((i) => list[i]!.id)
|
|
130
|
+
const nodes = new Map<string, TaskNode>()
|
|
131
|
+
for (let i = 0; i < rows.length; i++) {
|
|
132
|
+
const [, , , deps, adds, added] = rows[i]!
|
|
133
|
+
const node = list[i]!
|
|
134
|
+
node.deps = ids(deps)
|
|
135
|
+
if (adds != null) node.addsToOutputsOf = ids(adds)
|
|
136
|
+
if (added != null) node.outputsAddedToBy = added
|
|
137
|
+
nodes.set(node.id, node)
|
|
138
|
+
}
|
|
139
|
+
return nodes
|
|
140
|
+
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// CLI's `--affected` task selection (`vx show <task> --affected`), so both
|
|
3
3
|
// select over the graph the hooks leave.
|
|
4
4
|
|
|
5
|
+
import path from 'node:path'
|
|
5
6
|
import type { GraphRules, TaskNode } from '../graph/index.js'
|
|
7
|
+
import { relPosix, UserError } from '../util/index.js'
|
|
6
8
|
import { type ProjectMeta, validateProjectConfig } from '../workspace/index.js'
|
|
7
9
|
import { applyGraphHooks } from './plugin-host.js'
|
|
8
10
|
import type { VxPlugin } from './plugin.js'
|
|
9
|
-
import { isDefaultBuild } from '
|
|
11
|
+
import { isDefaultBuild } from '../graph/index.js'
|
|
10
12
|
|
|
11
13
|
export async function applyGraphStage(
|
|
12
14
|
plugins: readonly VxPlugin[],
|
|
@@ -23,6 +25,10 @@ export async function applyGraphStage(
|
|
|
23
25
|
// edit is held to what the loader accepts from a user: a misspelled
|
|
24
26
|
// `exec` field ran as an empty command and failed with no reason.
|
|
25
27
|
const configPaths = new Map(args.projectMetas.map((m) => [m.name, m.configPath]))
|
|
28
|
+
// A node under a directory no project owns, or another project's, ran,
|
|
29
|
+
// keyed and cleaned its outputs there: past the hard project boundary.
|
|
30
|
+
const dirs = new Map(args.projectMetas.map((m) => [m.name, path.resolve(m.dir)]))
|
|
31
|
+
const shown = (dir: string): string => relPosix(args.workspaceRoot, dir) || '.'
|
|
26
32
|
await applyGraphHooks(
|
|
27
33
|
plugins,
|
|
28
34
|
nodes,
|
|
@@ -34,6 +40,17 @@ export async function applyGraphStage(
|
|
|
34
40
|
},
|
|
35
41
|
(plugin) => {
|
|
36
42
|
for (const n of nodes.values()) {
|
|
43
|
+
const dir = dirs.get(n.projectName)
|
|
44
|
+
if (dir === undefined) {
|
|
45
|
+
throw new UserError(
|
|
46
|
+
`plugin '${plugin.name}' failed in graph: ${n.id}'s project ${n.projectName} is not a project in this workspace`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
if (path.resolve(n.projectDir) !== dir) {
|
|
50
|
+
throw new UserError(
|
|
51
|
+
`plugin '${plugin.name}' failed in graph: ${n.id}'s projectDir is ${shown(n.projectDir)}, not project ${n.projectName}'s directory ${shown(dir)}`,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
37
54
|
if (isDefaultBuild(n.config)) continue
|
|
38
55
|
const own = configPaths.get(n.projectName) ?? null
|
|
39
56
|
validateProjectConfig(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A server the run's local store cannot settle: some dependant missed here
|
|
2
2
|
// but may be a hit the remote holds. The scheduler holds such a server
|
|
3
3
|
// until the remote answers; when every dependant hit, the server never
|
|
4
|
-
// starts (
|
|
4
|
+
// starts (2026-10-22), as it never starts behind local hits.
|
|
5
5
|
|
|
6
6
|
import type { TaskNode } from '../graph/index.js'
|
|
7
7
|
import type { CacheEntry, CacheLayer } from '../cache/index.js'
|