@vzn/vx 0.0.637 → 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 +84 -28
- package/docs/cli.md +273 -408
- package/docs/execution.md +19 -44
- package/docs/features.md +13 -15
- package/docs/patterns.md +5 -7
- package/docs/schema.md +68 -61
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/schemas/plan.json +2 -2
- package/skills/vx/SKILL.md +19 -6
- package/src/bin.ts +13 -7
- package/src/cache/cache.ts +60 -11
- package/src/cache/git-inputs.ts +6 -0
- package/src/cache/index.ts +10 -2
- package/src/cache/inputs.ts +0 -24
- package/src/cache/layer.ts +32 -12
- package/src/cache/layered-cache.ts +112 -315
- package/src/cache/plugin-cache.ts +240 -0
- package/src/cache/policy.ts +9 -0
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +92 -8
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +44 -13
- package/src/cache/scoped-cache.ts +22 -2
- package/src/cli/cache.ts +33 -16
- package/src/cli/completions.ts +11 -7
- package/src/cli/foreign-flags.ts +10 -22
- package/src/cli/help.ts +22 -38
- package/src/cli/index.ts +25 -11
- package/src/cli/info.ts +8 -6
- package/src/cli/init.ts +14 -15
- package/src/cli/last.ts +20 -10
- package/src/cli/lock.ts +39 -27
- package/src/cli/plan-format.ts +10 -61
- package/src/cli/plugin-commands.ts +77 -6
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +31 -191
- package/src/cli/select.ts +18 -9
- package/src/cli/show.ts +72 -40
- package/src/cli/task-verb.ts +4 -4
- package/src/cli/upgrade.ts +132 -39
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch-filter.ts +7 -2
- package/src/cli/watch-judge.ts +6 -1
- package/src/cli/watch.ts +73 -34
- package/src/cli/why.ts +14 -5
- package/src/cli/workspace-config.ts +2 -2
- 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 +12 -16
- package/src/graph/task-graph.ts +119 -21
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +20 -11
- package/src/{orchestrator → history}/history.ts +61 -5
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +14 -23
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +21 -29
- package/src/orchestrator/affected-tasks.ts +8 -3
- package/src/orchestrator/doctor.ts +2 -2
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +47 -429
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +3 -74
- 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 +77 -47
- package/src/orchestrator/plugin.ts +86 -14
- package/src/orchestrator/prepare.ts +74 -31
- package/src/orchestrator/projects.ts +3 -0
- package/src/orchestrator/remote-prefetch.ts +23 -6
- 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 +36 -4
- 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 +313 -555
- 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 +6 -83
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +39 -34
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +166 -21
- package/src/{orchestrator → report}/status-line.ts +84 -7
- package/src/{orchestrator → report}/summary.ts +179 -63
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +13 -23
- package/src/{orchestrator → util}/colors.ts +49 -4
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +32 -0
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +25 -3
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +39 -0
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +197 -0
- package/src/workspace/affected.ts +20 -23
- package/src/workspace/dependency-rules.ts +12 -4
- package/src/workspace/migration.ts +87 -91
- package/src/workspace/workspace.ts +5 -4
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -276
- 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
package/src/cli/watch.ts
CHANGED
|
@@ -38,12 +38,12 @@ import {
|
|
|
38
38
|
import { CLOSED, fsClockNow, type WatchHandle, WatcherPool } from './watch-fs.js'
|
|
39
39
|
import { ChangeJudge } from './watch-judge.js'
|
|
40
40
|
import { type WatchCycle, watchCycle } from './watch-cycle.js'
|
|
41
|
-
import { hangupIgnored, maskedLine, restartTimings } from '../util/index.js'
|
|
41
|
+
import { hangupIgnored, maskedLine, restartTimings, sayNotice, sayLine } from '../util/index.js'
|
|
42
42
|
import { sweepConfigs, watchedProjects } from './watch-set.js'
|
|
43
43
|
|
|
44
44
|
/** One line for a watcher or re-read the OS refused; the loop goes on without it. */
|
|
45
45
|
function sayCannot(what: string, err: unknown): void {
|
|
46
|
-
|
|
46
|
+
sayNotice('warn', 'vx watch', `${what}: ${err instanceof Error ? err.message : String(err)}`)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Wait this long after the last filesystem event before re-running. */
|
|
@@ -61,22 +61,17 @@ const DEBOUNCE_MAX_MS = 1_000
|
|
|
61
61
|
* the help line and the completions, and a test holds the two together.
|
|
62
62
|
*/
|
|
63
63
|
export function watchRefusal(parsed: RunArgs): string | null {
|
|
64
|
-
if (parsed.dry !== undefined
|
|
65
|
-
return 'vx watch: --dry
|
|
64
|
+
if (parsed.dry !== undefined) {
|
|
65
|
+
return 'vx watch: --dry is not supported in watch mode'
|
|
66
66
|
}
|
|
67
|
-
if (parsed.
|
|
68
|
-
return 'vx watch: --
|
|
67
|
+
if (parsed.profile !== undefined) {
|
|
68
|
+
return 'vx watch: --profile is not supported in watch mode (would overwrite per cycle)'
|
|
69
69
|
}
|
|
70
|
-
//
|
|
71
|
-
//
|
|
70
|
+
// It formats ONE run's result and is consumed by `runCmd` alone, so a
|
|
71
|
+
// watch loop silently ignored it. `--verbosity 0` is not rejected: it
|
|
72
72
|
// asks for the output watch already gives.
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
parsed.reportFile !== undefined ||
|
|
76
|
-
parsed.verbosity > 0 ||
|
|
77
|
-
parsed.format !== 'pretty'
|
|
78
|
-
) {
|
|
79
|
-
return 'vx watch: --report / --report-file / --verbosity / --format are not supported in watch mode (they report a single run)'
|
|
73
|
+
if (parsed.verbosity > 0) {
|
|
74
|
+
return 'vx watch: --verbosity is not supported in watch mode (it reports a single run)'
|
|
80
75
|
}
|
|
81
76
|
return null
|
|
82
77
|
}
|
|
@@ -84,18 +79,20 @@ export function watchRefusal(parsed: RunArgs): string | null {
|
|
|
84
79
|
export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
85
80
|
const parsed = parseRunArgs(args, 'watch')
|
|
86
81
|
if (parsed.error) {
|
|
87
|
-
|
|
82
|
+
sayNotice('error', 'vx watch', `${parsed.error}${seeHelp('watch')}`)
|
|
88
83
|
return 1
|
|
89
84
|
}
|
|
90
85
|
|
|
91
86
|
const refused = watchRefusal(parsed)
|
|
92
87
|
if (refused !== null) {
|
|
93
|
-
|
|
88
|
+
sayLine('error', refused, 'vx watch')
|
|
94
89
|
return 1
|
|
95
90
|
}
|
|
96
91
|
if (parsed.tasks.length === 0) {
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
sayNotice(
|
|
93
|
+
'error',
|
|
94
|
+
'vx watch',
|
|
95
|
+
`missing task name (vx watch <task>, e.g. vx watch build)${seeHelp('watch')}`,
|
|
99
96
|
)
|
|
100
97
|
return 1
|
|
101
98
|
}
|
|
@@ -103,11 +100,11 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
103
100
|
const cwd = process.cwd()
|
|
104
101
|
const resolved = await resolveRunOptions(parsed, cwd, parsed.tasks, 'watch')
|
|
105
102
|
if ('error' in resolved) {
|
|
106
|
-
|
|
103
|
+
sayNotice('error', 'vx watch', resolved.error)
|
|
107
104
|
return 1
|
|
108
105
|
}
|
|
109
106
|
if ('nothingSelected' in resolved) {
|
|
110
|
-
|
|
107
|
+
sayNotice('info', 'vx watch', resolved.nothingSelected)
|
|
111
108
|
return 0
|
|
112
109
|
}
|
|
113
110
|
// The watch loop owns SIGINT/SIGTERM for its whole lifetime. A cycle's
|
|
@@ -216,7 +213,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
216
213
|
let refusedToStart = false
|
|
217
214
|
initial.held = runOrchestrator(initial.cycle.opts).then(
|
|
218
215
|
(run) => {
|
|
219
|
-
if (run.refused !== undefined)
|
|
216
|
+
if (run.refused !== undefined) sayNotice('error', 'vx watch', run.refused)
|
|
220
217
|
// A run that failed having run nothing refused to start: a requested
|
|
221
218
|
// name no project declares (run() says which, with a "did you mean").
|
|
222
219
|
// `vx run` exits 1 on it; `vx watch buidl` watched on, re-running the
|
|
@@ -227,8 +224,10 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
227
224
|
return run.persistent
|
|
228
225
|
},
|
|
229
226
|
(err: unknown) => {
|
|
230
|
-
|
|
231
|
-
|
|
227
|
+
sayNotice(
|
|
228
|
+
'error',
|
|
229
|
+
'vx watch',
|
|
230
|
+
`cycle failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
232
231
|
)
|
|
233
232
|
return undefined
|
|
234
233
|
},
|
|
@@ -247,7 +246,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
|
|
|
247
246
|
// run's own refusal and its "did you mean".
|
|
248
247
|
if (scope.length === 0 && !unlocked) {
|
|
249
248
|
await held?.stop()
|
|
250
|
-
|
|
249
|
+
sayNotice('error', 'vx watch', 'no projects in scope')
|
|
251
250
|
return 1
|
|
252
251
|
}
|
|
253
252
|
await armLoop(undefined, held)
|
|
@@ -491,7 +490,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
491
490
|
held = cycle.persistent
|
|
492
491
|
// What this cycle did not take (a server whose task did not run).
|
|
493
492
|
await before?.stop()
|
|
494
|
-
if (cycle.refused !== undefined)
|
|
493
|
+
if (cycle.refused !== undefined) sayNotice('error', 'vx watch', cycle.refused)
|
|
495
494
|
changes.lastCycle = { start, end: Date.now() }
|
|
496
495
|
} catch (err) {
|
|
497
496
|
// A re-run can fail catastrophically when the workspace
|
|
@@ -503,7 +502,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
503
502
|
// is the canonical exit; we don't unilaterally abort here.
|
|
504
503
|
// A config's own throw or a plugin's failure may quote a secret (L-11).
|
|
505
504
|
const message = maskedLine(err instanceof Error ? err.message : String(err))
|
|
506
|
-
|
|
505
|
+
sayNotice('error', 'vx watch', `cycle failed: ${message}`)
|
|
507
506
|
}
|
|
508
507
|
// After a failed cycle too: a package added with a config that does
|
|
509
508
|
// not load yet fails its cycle, and unarmed, the fix to that config
|
|
@@ -622,6 +621,19 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
622
621
|
}
|
|
623
622
|
}
|
|
624
623
|
const stale = (dir: string): boolean => armedAs.get(dir) !== inodeOf(dir)
|
|
624
|
+
/** The projects the workspace-wide arm names an event by. */
|
|
625
|
+
let wide: readonly ProjectMeta[] = []
|
|
626
|
+
// The deepest, as a nested project's file is its own.
|
|
627
|
+
const ownerOf = (abs: string): ProjectMeta | undefined => {
|
|
628
|
+
let best: ProjectMeta | undefined
|
|
629
|
+
for (const proj of wide)
|
|
630
|
+
if (
|
|
631
|
+
abs.startsWith(proj.dir + path.sep) &&
|
|
632
|
+
(best === undefined || proj.dir.length > best.dir.length)
|
|
633
|
+
)
|
|
634
|
+
best = proj
|
|
635
|
+
return best
|
|
636
|
+
}
|
|
625
637
|
/** The root arm: recursive when workspace-wide or the one stream, else the root's own files only. */
|
|
626
638
|
let rootArm: WatchHandle = CLOSED
|
|
627
639
|
let dotVxArm: WatchHandle = CLOSED
|
|
@@ -696,7 +708,13 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
696
708
|
configImportFiles.includes(abs)
|
|
697
709
|
)
|
|
698
710
|
reread = true
|
|
699
|
-
|
|
711
|
+
const owner = ownerOf(abs)
|
|
712
|
+
trigger(
|
|
713
|
+
owner === undefined
|
|
714
|
+
? `root ${filename}`
|
|
715
|
+
: `${owner.name} ${path.relative(owner.dir, abs)}`,
|
|
716
|
+
abs,
|
|
717
|
+
)
|
|
700
718
|
})
|
|
701
719
|
: oneStream
|
|
702
720
|
? arm(workspaceRoot, true, streamEvent)
|
|
@@ -721,6 +739,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
721
739
|
// (it would cover every project + node_modules + caches). On macOS one
|
|
722
740
|
// recursive stream at the root stands for all of them (`oneStream`).
|
|
723
741
|
const armMode = (watched: readonly ProjectMeta[]): void => {
|
|
742
|
+
wide = workspaceWide ? watched : []
|
|
724
743
|
if (!workspaceWide) {
|
|
725
744
|
streamed = oneStream ? toStream(watched) : []
|
|
726
745
|
const inStream = new Set(streamed.map((s) => s.proj))
|
|
@@ -797,12 +816,18 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
797
816
|
armAncestors()
|
|
798
817
|
}
|
|
799
818
|
|
|
800
|
-
// Said once the loop
|
|
801
|
-
//
|
|
819
|
+
// Said once, when the loop first arms: a re-arm repeated it after every
|
|
820
|
+
// cycle that re-read the watched set, between task rows. `debug.watch`
|
|
821
|
+
// says each arm on stderr, for whoever needs to see the set change.
|
|
802
822
|
let lastTasks: number | undefined
|
|
823
|
+
const plural = (n: number, noun: string): string => `${n} ${noun}${n === 1 ? '' : 's'}`
|
|
803
824
|
const sayWatching = (count: number): void => {
|
|
804
|
-
const tasks = lastTasks === undefined ? '' : ` · ${lastTasks
|
|
805
|
-
process.stdout.write(`watching ${count
|
|
825
|
+
const tasks = lastTasks === undefined ? '' : ` · ${plural(lastTasks, 'task')}`
|
|
826
|
+
process.stdout.write(`watching ${plural(count, 'project')}${tasks}\n`)
|
|
827
|
+
}
|
|
828
|
+
const debugArms = resolveSetting('debug.watch', undefined).value === true
|
|
829
|
+
const sayArmed = (count: number): void => {
|
|
830
|
+
if (debugArms) process.stderr.write(`vx watch: armed ${plural(count, 'project')}\n`)
|
|
806
831
|
}
|
|
807
832
|
|
|
808
833
|
const rearm = async (say = true): Promise<void> => {
|
|
@@ -854,7 +879,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
854
879
|
// A new arm proves delivery like the first ones: an edit in the new
|
|
855
880
|
// project right after this cycle is seen, not lost in the gap.
|
|
856
881
|
await pool.proved()
|
|
857
|
-
if (say)
|
|
882
|
+
if (say) sayArmed(next.projects.length)
|
|
858
883
|
}
|
|
859
884
|
|
|
860
885
|
armMode(projects)
|
|
@@ -872,10 +897,18 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
872
897
|
// never heard `packages/` removed under `packages/a` (inotify did).
|
|
873
898
|
const ancestorArms = new Map<string, WatchHandle>()
|
|
874
899
|
let ancestorNames = new Map<string, Set<string>>()
|
|
900
|
+
/** Each project directory below the root as these arms first saw it (`inodeOf`). */
|
|
901
|
+
let heldDirs = new Map<string, string>()
|
|
875
902
|
const armAncestors = (): void => {
|
|
876
903
|
ancestorNames = new Map()
|
|
877
904
|
const rootDir = path.resolve(workspaceRoot)
|
|
878
905
|
const below = projectDirs.filter((d) => path.resolve(d) !== rootDir)
|
|
906
|
+
heldDirs = new Map(
|
|
907
|
+
below.flatMap((d) => {
|
|
908
|
+
const id = inodeOf(d)
|
|
909
|
+
return id === undefined ? [] : [[d, id]]
|
|
910
|
+
}),
|
|
911
|
+
)
|
|
879
912
|
for (const base of [...below, ...importNames.keys()]) {
|
|
880
913
|
let dir = path.dirname(base)
|
|
881
914
|
let name = path.basename(base)
|
|
@@ -907,8 +940,13 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
907
940
|
arm(dir, false, (filename) => {
|
|
908
941
|
// A project's parent's `.gitignore` shapes what its keys read, as the root's does.
|
|
909
942
|
if (!ancestorNames.get(dir)?.has(filename) && filename !== '.gitignore') return
|
|
910
|
-
reread = true
|
|
911
943
|
const abs = path.join(dir, filename)
|
|
944
|
+
// A project directory still the one its arm holds is no change:
|
|
945
|
+
// its arm judges what is in it. macOS reports the directory's
|
|
946
|
+
// mtime here on every write at its top level, and a cycle's own
|
|
947
|
+
// write started the next (solid's `types`, M 2026-10-15).
|
|
948
|
+
if (heldDirs.get(abs) === (inodeOf(abs) ?? '')) return
|
|
949
|
+
reread = true
|
|
912
950
|
trigger(path.relative(workspaceRoot, abs), abs)
|
|
913
951
|
}),
|
|
914
952
|
)
|
|
@@ -937,6 +975,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<void> {
|
|
|
937
975
|
|
|
938
976
|
lastTasks = args.tasks()
|
|
939
977
|
sayWatching(projects.length)
|
|
978
|
+
sayArmed(projects.length)
|
|
940
979
|
|
|
941
980
|
return await new Promise<void>((resolve) => {
|
|
942
981
|
const cleanup = async (): Promise<void> => {
|
package/src/cli/why.ts
CHANGED
|
@@ -9,14 +9,14 @@ import { flagHint, formatValue, seeHelp } from './help.js'
|
|
|
9
9
|
import { splitTaskId } from '../graph/index.js'
|
|
10
10
|
import {
|
|
11
11
|
cacheKeyDiff,
|
|
12
|
-
|
|
12
|
+
explainCacheKey,
|
|
13
13
|
inputWriters,
|
|
14
14
|
latestRunId,
|
|
15
15
|
rootCauses,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from '../orchestrator/index.js'
|
|
19
|
-
import { MASKED, nearMatches, printable, UserError } from '../util/index.js'
|
|
16
|
+
whyDidThisRerun,
|
|
17
|
+
} from '../history/index.js'
|
|
18
|
+
import { resolveRunId } from '../orchestrator/index.js'
|
|
19
|
+
import { MASKED, nearMatches, printable, refusal, UserError } from '../util/index.js'
|
|
20
20
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
21
21
|
import { findCwdProject } from './select.js'
|
|
22
22
|
import { cliCacheDir, discoverCliProjects, parseCacheDirFlag } from './workspace-config.js'
|
|
@@ -145,6 +145,15 @@ async function resolveTarget(
|
|
|
145
145
|
.all() as Array<{ project: string; task: string }>
|
|
146
146
|
const ids = pairs.map((p) => `${p.project}#${p.task}`)
|
|
147
147
|
const invoked = ids.length === 0 && db.query('SELECT 1 FROM invocations LIMIT 1').get() !== null
|
|
148
|
+
if (ids.length === 0 && !invoked) {
|
|
149
|
+
throw refusal(
|
|
150
|
+
{ tone: 'info', headline: `no runs of ${target} yet`, fix: `vx run ${target}` },
|
|
151
|
+
{
|
|
152
|
+
code: 'VX_E_NO_HISTORY',
|
|
153
|
+
message: `vx why: no recorded runs for "${target}"${suggest(target, ids, invoked)}`,
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
}
|
|
148
157
|
if (target.includes('#')) {
|
|
149
158
|
throw new UserError(
|
|
150
159
|
`vx why: no recorded runs for "${target}"${suggest(target, ids, invoked)}`,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import type { WorkspaceConfig } from '../config.js'
|
|
10
|
-
import { UserError } from '../util/index.js'
|
|
10
|
+
import { UserError, sayLine } from '../util/index.js'
|
|
11
11
|
import {
|
|
12
12
|
discoverProjects,
|
|
13
13
|
loadProjects,
|
|
@@ -33,7 +33,7 @@ interface CliWorkspace {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export const warnToStderr = (message: string): void => {
|
|
36
|
-
|
|
36
|
+
sayLine('warn', message)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorkspace> {
|
package/src/config.ts
CHANGED
|
@@ -62,12 +62,6 @@ export interface RunSettings {
|
|
|
62
62
|
* default) skips its dependents, `always` runs them too.
|
|
63
63
|
*/
|
|
64
64
|
continue?: 'never' | 'deps-ok' | 'always'
|
|
65
|
-
/** How long a task that ignores SIGTERM gets before SIGKILL. Omitted → `'2s'`. */
|
|
66
|
-
killGrace?: Duration
|
|
67
|
-
/** The bound on each plugin's end-of-run flush and teardown. Omitted → `'3s'`. */
|
|
68
|
-
teardownTimeout?: Duration
|
|
69
|
-
/** How long a persistent task may take to match `readyWhen` before vx says it waits. Omitted → `'10s'`. */
|
|
70
|
-
readyNotice?: Duration
|
|
71
65
|
/**
|
|
72
66
|
* Graph rules checked by `vx lock` and `vx lock --check`, never by a
|
|
73
67
|
* run. Each is on
|
package/src/exec/executor.ts
CHANGED
|
@@ -142,7 +142,7 @@ export interface ExecuteRequest {
|
|
|
142
142
|
* work and returns: a child an executor spawned is not one core can
|
|
143
143
|
* reach (H-10, H-12). One already aborted is work not to start (B-55).
|
|
144
144
|
* Any exit after the timeout's abort, a 0 included, is a timeout. Core
|
|
145
|
-
* waits the kill grace (
|
|
145
|
+
* waits the kill grace (2 s) after the abort, then
|
|
146
146
|
* settles the attempt without `execute` and abandons it (H-14).
|
|
147
147
|
*/
|
|
148
148
|
readonly signal?: AbortSignal
|
|
@@ -2332,7 +2332,7 @@ function outsideWritesHint(
|
|
|
2332
2332
|
? `${refused} The task has its own temp directory, empty at its start: write under ` +
|
|
2333
2333
|
`$TMPDIR (os.tmpdir() in Node and Bun) instead of a fixed path.`
|
|
2334
2334
|
: `${refused} If the task needs one, grant its directory, e.g. ` +
|
|
2335
|
-
`\`allow: { write: [${jsString(`${spelled}/`)}] }\`.`
|
|
2335
|
+
`\`allow: { write: [${jsString(spelled.endsWith('/') ? spelled : `${spelled}/`)}] }\`.`
|
|
2336
2336
|
return { timestamp: new Date(), hint: true, line }
|
|
2337
2337
|
}
|
|
2338
2338
|
|
|
@@ -578,7 +578,9 @@ export function refusedWritesOutside(
|
|
|
578
578
|
if (v.path === undefined || !path.isAbsolute(v.path) || !write || reported(v)) continue
|
|
579
579
|
if (!skip.some((s) => atOrUnder(v.path!, s))) paths.add(v.path)
|
|
580
580
|
}
|
|
581
|
-
|
|
581
|
+
// `mkdir -p ~/.cache/tool` is refused at each directory on its way;
|
|
582
|
+
// the one it wanted is the deepest.
|
|
583
|
+
return [...paths].filter((p) => ![...paths].some((q) => q !== p && atOrUnder(q, p)))
|
|
582
584
|
}
|
|
583
585
|
|
|
584
586
|
/**
|
package/src/graph/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export {
|
|
|
8
8
|
parseDependencySpec,
|
|
9
9
|
type DependencySpec,
|
|
10
10
|
} from './dependency-spec.js'
|
|
11
|
+
export { computeReverseDepCount } from './priorities.js'
|
|
11
12
|
export {
|
|
12
13
|
type ContinueMode,
|
|
13
14
|
deadServerBehind,
|
|
@@ -16,8 +17,15 @@ export {
|
|
|
16
17
|
RestoreDemoted,
|
|
17
18
|
runGraph,
|
|
18
19
|
type TaskOutcome,
|
|
19
|
-
type TaskStatus,
|
|
20
20
|
} from './scheduler.js'
|
|
21
|
+
export {
|
|
22
|
+
type CacheSource,
|
|
23
|
+
deriveCacheSource,
|
|
24
|
+
isCacheHit,
|
|
25
|
+
isPassStatus,
|
|
26
|
+
TASK_STATUSES,
|
|
27
|
+
type TaskStatus,
|
|
28
|
+
} from './task-status.js'
|
|
21
29
|
export {
|
|
22
30
|
buildTaskGraph,
|
|
23
31
|
checkGraph,
|
package/src/graph/priorities.ts
CHANGED
|
@@ -85,7 +85,7 @@ export function tieredReverseDepCount(
|
|
|
85
85
|
* dependents only (`calculateReverseDeps` in
|
|
86
86
|
* `packages/nx/src/tasks-runner/utils.ts`, read 2026-09-24).
|
|
87
87
|
*/
|
|
88
|
-
export function computeReverseDepCount(nodes:
|
|
88
|
+
export function computeReverseDepCount(nodes: ReadonlyMap<string, TaskNode>): Map<string, number> {
|
|
89
89
|
// Exact transitive-dependent COUNTS need closure SETS (diamonds
|
|
90
90
|
// double-count under naive summing). Set-of-strings closures are
|
|
91
91
|
// O(N²) entries and took 8.5s on a 1090-package, 100-layer repo;
|
package/src/graph/scheduler.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ScheduleStrategy,
|
|
14
14
|
} from './priorities.js'
|
|
15
15
|
import { isGroupTask, type TaskNode } from './task-graph.js'
|
|
16
|
+
import type { TaskStatus } from './task-status.js'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Thrown by `execute` for a restore-tier task that turned out to have
|
|
@@ -29,19 +30,6 @@ export class RestoreDemoted extends Error {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export type TaskStatus =
|
|
33
|
-
| 'success'
|
|
34
|
-
| 'cache-hit'
|
|
35
|
-
| 'cache-hit-remote'
|
|
36
|
-
| 'failed'
|
|
37
|
-
| 'skipped'
|
|
38
|
-
// A child killed by a shutdown signal (SIGINT/SIGTERM — e.g. Ctrl-C):
|
|
39
|
-
// the task never finished on its own terms, so it reverts to aborted —
|
|
40
|
-
// excluded from the tally and the run history. It still PROPAGATES like
|
|
41
|
-
// a failure (see `willSkip`): the child died mid-write, so its outputs
|
|
42
|
-
// are partial.
|
|
43
|
-
| 'aborted'
|
|
44
|
-
|
|
45
33
|
/** What moved in a missed task's key since the last entry saved for it. */
|
|
46
34
|
export interface InputChanges {
|
|
47
35
|
/** How many components changed, appeared or went. 0: the same key, its entry gone. */
|
|
@@ -79,8 +67,8 @@ export interface TaskOutcome {
|
|
|
79
67
|
* How long an `admit` policy held this task after it was ready with a
|
|
80
68
|
* free worker — the wait the plugin, not the count gate, imposed. Set
|
|
81
69
|
* only when a policy refused it at least once (so a run with no policy
|
|
82
|
-
* carries the field on no outcome);
|
|
83
|
-
*
|
|
70
|
+
* carries the field on no outcome); the event stream and the summary
|
|
71
|
+
* footer show it.
|
|
84
72
|
*/
|
|
85
73
|
admissionHeldMs?: number
|
|
86
74
|
/**
|
|
@@ -136,7 +124,7 @@ export interface TaskOutcome {
|
|
|
136
124
|
* On a `skipped` outcome: the task at the ROOT of what blocked it — the
|
|
137
125
|
* failed (or aborted) upstream, followed through any chain of skips
|
|
138
126
|
* between. Absent when the skip was fail-fast's (no upstream failed).
|
|
139
|
-
* The footer,
|
|
127
|
+
* The footer, the telemetry record and the run row all
|
|
140
128
|
* read this one field (item 267).
|
|
141
129
|
*/
|
|
142
130
|
blockedBy?: string
|
|
@@ -152,6 +140,12 @@ export interface TaskOutcome {
|
|
|
152
140
|
* bounded to its first 8 KiB and last 56 KiB. The run's history keeps it.
|
|
153
141
|
*/
|
|
154
142
|
failedOutput?: string
|
|
143
|
+
/**
|
|
144
|
+
* When the run keeps its bytes in a remote, a task that ran and saved no
|
|
145
|
+
* entry (failed, or uncached): its whole output as `encodeOutputLog`
|
|
146
|
+
* stores it, secrets masked, at the entry's bounds.
|
|
147
|
+
*/
|
|
148
|
+
runLog?: string
|
|
155
149
|
/**
|
|
156
150
|
* On a failed persistent task: why it never became ready — the readiness
|
|
157
151
|
* deadline fired, the child exited first (`exitCode` is then its own), or
|
|
@@ -171,6 +165,8 @@ export interface TaskOutcome {
|
|
|
171
165
|
* (`--cache-download=none`) instead of landing on this machine. Absent for
|
|
172
166
|
* every ordinary outcome. Telemetry-only. */
|
|
173
167
|
outputs?: 'deferred'
|
|
168
|
+
/** The task ran here and its result went to the cache. The footer's `↑`. */
|
|
169
|
+
saved?: true
|
|
174
170
|
/**
|
|
175
171
|
* v11 analytics: hrtime span relative to the parent run's t=0.
|
|
176
172
|
* Lets downstream analytics reconstruct the actual parallel timeline
|