@vzn/vx 0.0.646 → 0.0.649
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
package/src/history/metrics.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface RunSummaryRow {
|
|
|
63
63
|
forwarded: boolean
|
|
64
64
|
/** Each remote read or write of its key that failed, `upload: <message>`, `; `-joined (v40). */
|
|
65
65
|
remoteError: string | null
|
|
66
|
+
/** The attempts a retried task took (>1, v23); null when it ran once. */
|
|
67
|
+
attempts: number | null
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export interface ListRunsArgs {
|
|
@@ -124,7 +126,7 @@ export function listRuns(db: Database, args: ListRunsArgs = {}): RunSummaryRow[]
|
|
|
124
126
|
wallclock_start_ns AS wallclockStartNs, wallclock_end_ns AS wallclockEndNs,
|
|
125
127
|
blocked_by AS blockedBy, timed_out AS timedOut,
|
|
126
128
|
sandbox_violations AS sandboxViolations, not_ready AS notReady,
|
|
127
|
-
forward_args IS NOT NULL AS forwarded, remote_error AS remoteError
|
|
129
|
+
forward_args IS NOT NULL AS forwarded, remote_error AS remoteError, attempts
|
|
128
130
|
FROM runs ${clause} ORDER BY id DESC LIMIT ?`,
|
|
129
131
|
)
|
|
130
132
|
.all(...params, limit) as RawRow[]
|
|
@@ -292,7 +294,9 @@ export interface RunDetail {
|
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
export function getRun(db: Database, runId: string): RunDetail | null {
|
|
295
|
-
|
|
297
|
+
// In the order the run recorded them, as it printed them: `listRuns` is
|
|
298
|
+
// newest first, and the replay listed a dependency below its dependant.
|
|
299
|
+
const tasks = listRuns(db, { runId, limit: 100_000 }).reverse()
|
|
296
300
|
if (tasks.length === 0) return null
|
|
297
301
|
const startedAt = Math.min(...tasks.map((t) => t.startedAt))
|
|
298
302
|
const endedAt = Math.max(...tasks.map((t) => t.endedAt))
|
|
@@ -385,6 +389,18 @@ export interface WhyDidThisRerun {
|
|
|
385
389
|
note: string
|
|
386
390
|
}
|
|
387
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Failed tasks in a run that wrote the cache: a passing task beside one
|
|
394
|
+
* ran past a failed dependency (--continue) and was never saved.
|
|
395
|
+
*/
|
|
396
|
+
function failedWithWrites(db: Database, id: string | null): number {
|
|
397
|
+
if (id === null) return 0
|
|
398
|
+
const row = db
|
|
399
|
+
.query('SELECT cache_policy AS p, failed_count AS f FROM invocations WHERE run_id = ?')
|
|
400
|
+
.get(id) as { p: string; f: number } | null
|
|
401
|
+
return row !== null && writesCache(row.p) ? row.f : 0
|
|
402
|
+
}
|
|
403
|
+
|
|
388
404
|
/**
|
|
389
405
|
* The endings an UNCHANGED cache key can have. A re-execution is told apart
|
|
390
406
|
* by the evidence the index holds: every one used to blame `--no-cache` /
|
|
@@ -396,7 +412,7 @@ function unchangedKeyNote(
|
|
|
396
412
|
db: Database,
|
|
397
413
|
runId: string,
|
|
398
414
|
this_: { hash: string; status: string; cacheHit: number | null; startedAt: number },
|
|
399
|
-
prev: { status: string; cacheHit: number | null; runId: string | null },
|
|
415
|
+
prev: { status: string; cacheHit: number | null; runId: string | null; blockedBy: string | null },
|
|
400
416
|
): string {
|
|
401
417
|
if (this_.cacheHit === null) {
|
|
402
418
|
return 'cache key unchanged — this run recorded no cache outcome, so whether it re-ran is unknown'
|
|
@@ -410,8 +426,12 @@ function unchangedKeyNote(
|
|
|
410
426
|
p: string
|
|
411
427
|
} | null
|
|
412
428
|
)?.p
|
|
429
|
+
// `--no-cache` reads and writes nothing, `--force` writes: each named
|
|
430
|
+
// apart, or a `--no-cache` run was told it had passed `--force`.
|
|
413
431
|
if (policy !== undefined && !readsCache(policy)) {
|
|
414
|
-
return
|
|
432
|
+
return writesCache(policy)
|
|
433
|
+
? 'cache key unchanged — re-executed because this run did not read the cache (--force, or a --cache-policy without read)'
|
|
434
|
+
: 'cache key unchanged — re-executed because this run did not use the cache (--no-cache, or a --cache-policy without read or write)'
|
|
415
435
|
}
|
|
416
436
|
// `get` answers null, not undefined, for no row.
|
|
417
437
|
const createdAt = (
|
|
@@ -431,17 +451,9 @@ function unchangedKeyNote(
|
|
|
431
451
|
prev.status === 'success' &&
|
|
432
452
|
prev.cacheHit === 0
|
|
433
453
|
) {
|
|
434
|
-
const
|
|
435
|
-
if (id === null) return 0
|
|
436
|
-
const row = db
|
|
437
|
-
.query('SELECT cache_policy AS p, failed_count AS f FROM invocations WHERE run_id = ?')
|
|
438
|
-
.get(id) as { p: string; f: number } | null
|
|
439
|
-
const writes = row !== null && writesCache(row.p)
|
|
440
|
-
return writes ? (row?.f ?? 0) : 0
|
|
441
|
-
}
|
|
442
|
-
const before = failedWithWrites(prev.runId)
|
|
454
|
+
const before = failedWithWrites(db, prev.runId)
|
|
443
455
|
if (before > 0) {
|
|
444
|
-
const now = failedWithWrites(runId)
|
|
456
|
+
const now = failedWithWrites(db, runId)
|
|
445
457
|
if (createdAt === undefined && now > 0) {
|
|
446
458
|
return `cache key unchanged — neither run saved it: each ran beside a failed task (${now} failed in this one), and a task run past a failed dependency (--continue) is never cached`
|
|
447
459
|
}
|
|
@@ -473,6 +485,18 @@ function unchangedKeyNote(
|
|
|
473
485
|
if (prevPolicy !== undefined && !writesCache(prevPolicy)) {
|
|
474
486
|
return 'cache key unchanged — the previous run on this key did not write the cache (--no-cache, or a --cache-policy without write), so there was nothing to hit'
|
|
475
487
|
}
|
|
488
|
+
// A task that passed behind a dependency its run never recorded: the
|
|
489
|
+
// one `--exclude-dependencies` left out, keyed but not run, so nothing
|
|
490
|
+
// built on it was saved (excluded-keys.ts).
|
|
491
|
+
if (
|
|
492
|
+
prev.blockedBy !== null &&
|
|
493
|
+
prev.runId !== null &&
|
|
494
|
+
db
|
|
495
|
+
.query('SELECT 1 FROM runs WHERE run_id = ? AND project = ? AND task = ?')
|
|
496
|
+
.get(prev.runId, ...splitTaskId(prev.blockedBy)) === null
|
|
497
|
+
) {
|
|
498
|
+
return `cache key unchanged — the previous run on this key built on a dependency it left out (${prev.blockedBy}, --exclude-dependencies), so it saved nothing and there was nothing to hit`
|
|
499
|
+
}
|
|
476
500
|
return 'cache key unchanged — the previous run on this key executed but no entry for it is in the cache (its save failed, or it was pruned since), so there was nothing to hit'
|
|
477
501
|
}
|
|
478
502
|
return 'cache key unchanged — re-executed on the same key though this run read the cache; vx cannot name the cause'
|
|
@@ -497,7 +521,7 @@ export function latestRunId(db: Database, taskId: string): string | null {
|
|
|
497
521
|
function keylessNote(taskId: string, keylessBy: string): string {
|
|
498
522
|
return keylessBy === taskId
|
|
499
523
|
? 'never cached: this task declares no `inputs`, so each run derives a new key; declare its `inputs`'
|
|
500
|
-
: `never cached: its key
|
|
524
|
+
: `never cached: its key includes ${keylessBy}, which declares no \`inputs\`, so each run derives a new key; declare inputs on ${keylessBy}`
|
|
501
525
|
}
|
|
502
526
|
|
|
503
527
|
export function whyDidThisRerun(db: Database, runId: string, taskId: string): WhyDidThisRerun {
|
|
@@ -535,7 +559,7 @@ export function whyDidThisRerun(db: Database, runId: string, taskId: string): Wh
|
|
|
535
559
|
const prev = db
|
|
536
560
|
.query(
|
|
537
561
|
`SELECT hash, status, cache_hit AS cacheHit, restored, started_at AS startedAt,
|
|
538
|
-
run_id AS runId
|
|
562
|
+
run_id AS runId, blocked_by AS blockedBy
|
|
539
563
|
FROM runs WHERE project = ? AND task = ? AND id < ? AND ${KEYED_RUNS_SQL}
|
|
540
564
|
ORDER BY id DESC LIMIT 1`,
|
|
541
565
|
)
|
|
@@ -547,6 +571,7 @@ export function whyDidThisRerun(db: Database, runId: string, taskId: string): Wh
|
|
|
547
571
|
restored: number | null
|
|
548
572
|
startedAt: number
|
|
549
573
|
runId: string | null
|
|
574
|
+
blockedBy: string | null
|
|
550
575
|
}
|
|
551
576
|
| undefined
|
|
552
577
|
// …and this run must have one too, or there is nothing to compare.
|
|
@@ -577,8 +602,14 @@ export function whyDidThisRerun(db: Database, runId: string, taskId: string): Wh
|
|
|
577
602
|
? 'this task recorded no cache key (skipped, or a persistent task) — nothing to compare'
|
|
578
603
|
: this_.cached === 0
|
|
579
604
|
? // Not a cache decision at all: the task runs every time by design,
|
|
580
|
-
// and its key is derived only so dependents can fold it.
|
|
581
|
-
'
|
|
605
|
+
// and its key is derived only so dependents can fold it. A keyless
|
|
606
|
+
// one's is new each run, so every dependent misses: "folded by
|
|
607
|
+
// dependents" read as if they could hit.
|
|
608
|
+
this_.keylessBy === null
|
|
609
|
+
? 'this task declares no `cache` block — it runs on every invocation; its key is folded by dependents only'
|
|
610
|
+
: this_.keylessBy === taskId
|
|
611
|
+
? 'this task declares no `cache` block — it runs on every invocation; it declares no `inputs` either, so each run derives a new key and every task that depends on it misses; declare its `inputs`'
|
|
612
|
+
: `this task declares no \`cache\` block — it runs on every invocation; its key includes ${this_.keylessBy}, which declares no \`inputs\`, so each run derives a new key and every task that depends on it misses; declare inputs on ${this_.keylessBy}`
|
|
582
613
|
: this_.keylessBy !== null
|
|
583
614
|
? keylessNote(taskId, this_.keylessBy)
|
|
584
615
|
: prev && prev.hash !== this_.hash
|
|
@@ -635,7 +666,7 @@ export function loadEntryInputs(db: Database, entryHash: string): TaskInputCompo
|
|
|
635
666
|
}
|
|
636
667
|
|
|
637
668
|
/**
|
|
638
|
-
* The rule `vx why`
|
|
669
|
+
* The rule `vx why` names a moved key by: join
|
|
639
670
|
* two keys' components over `(kind, name)`. A component in both with a
|
|
640
671
|
* different hash is `changed`, one only in `after` is `added`, one only in
|
|
641
672
|
* `before` is `removed`, and an equal one is counted. Entries are ordered by
|
|
@@ -677,8 +708,7 @@ export function diffKeyComponents(
|
|
|
677
708
|
}
|
|
678
709
|
}
|
|
679
710
|
// Code-unit order, not `localeCompare`: collation follows the machine's
|
|
680
|
-
// locale,
|
|
681
|
-
// so the CLI and the page could list one diff in two orders. Code units
|
|
711
|
+
// locale, so two machines could list one diff in two orders. Code units
|
|
682
712
|
// are also SQLite's BINARY order, the `entry_inputs` scan's own.
|
|
683
713
|
const byCodeUnit = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
|
|
684
714
|
entries.sort((x, y) => byCodeUnit(x.kind, y.kind) || byCodeUnit(x.name, y.name))
|
|
@@ -797,7 +827,9 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
797
827
|
// A failed previous run kept no fingerprints; the last run that saved an
|
|
798
828
|
// entry still names what moved since then (the edit an agent made to fix
|
|
799
829
|
// the failure, or the one that broke it).
|
|
800
|
-
|
|
830
|
+
// So did one that passed beside a failed task (--continue).
|
|
831
|
+
const tainted = isPassStatus(prev.status) && failedWithWrites(db, prev.runId) > 0
|
|
832
|
+
if (cur.length > 0 && old.length === 0 && (!isPassStatus(prev.status) || tainted)) {
|
|
801
833
|
const saved = db
|
|
802
834
|
.query(
|
|
803
835
|
`SELECT run_id AS runId, hash FROM runs
|
|
@@ -808,7 +840,7 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
808
840
|
.get(project, task, this_.id) as { runId: string | null; hash: string } | undefined
|
|
809
841
|
if (saved) {
|
|
810
842
|
const { entries, unchangedCount } = diffKeyComponents(loadEntryInputs(db, saved.hash), cur)
|
|
811
|
-
const since = `run ${saved.runId}, the last that saved an entry (the previous run ended ${prev.status})`
|
|
843
|
+
const since = `run ${saved.runId}, the last that saved an entry (${tainted ? 'the previous run ran beside a failed task, so it was not saved' : `the previous run ended ${prev.status}`})`
|
|
812
844
|
return {
|
|
813
845
|
runId,
|
|
814
846
|
taskId,
|
|
@@ -835,7 +867,9 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
835
867
|
? `this run ended ${this_.status}`
|
|
836
868
|
: old.length === 0 && !isPassStatus(prev.status)
|
|
837
869
|
? `the previous run ended ${prev.status}`
|
|
838
|
-
:
|
|
870
|
+
: old.length === 0 && tainted
|
|
871
|
+
? 'the previous run ran beside a failed task'
|
|
872
|
+
: null
|
|
839
873
|
return {
|
|
840
874
|
runId,
|
|
841
875
|
taskId,
|
|
@@ -870,6 +904,18 @@ export function cacheKeyDiff(db: Database, runId: string, taskId: string): Cache
|
|
|
870
904
|
}
|
|
871
905
|
}
|
|
872
906
|
|
|
907
|
+
/**
|
|
908
|
+
* Joins a group and a task under it in an `upstream` row's name: a group
|
|
909
|
+
* records no run, so its dependant's key names its members through it.
|
|
910
|
+
*/
|
|
911
|
+
export const GROUP_VIA = ' \u2190 '
|
|
912
|
+
|
|
913
|
+
/** The task an `upstream` row names: the last one, past any group it came through. */
|
|
914
|
+
export function upstreamTask(name: string): string {
|
|
915
|
+
const i = name.lastIndexOf(GROUP_VIA)
|
|
916
|
+
return i < 0 ? name : name.slice(i + GROUP_VIA.length)
|
|
917
|
+
}
|
|
918
|
+
|
|
873
919
|
export interface RootCause {
|
|
874
920
|
/** From the asked task down to the one whose own inputs moved. */
|
|
875
921
|
chain: string[]
|
|
@@ -879,23 +925,37 @@ export interface RootCause {
|
|
|
879
925
|
/**
|
|
880
926
|
* The tasks under `taskId`'s moved upstreams whose OWN key components
|
|
881
927
|
* moved in the same run, each with the chain that carried it up. A task
|
|
882
|
-
* visited once is not walked again (a diamond names its root once).
|
|
928
|
+
* visited once is not walked again (a diamond names its root once). A
|
|
929
|
+
* group records no run, so `groups` (group id → the tasks it gathers)
|
|
930
|
+
* carries the walk through it.
|
|
883
931
|
*/
|
|
884
932
|
export function rootCauses(
|
|
885
933
|
db: Database,
|
|
886
934
|
runId: string,
|
|
887
935
|
taskId: string,
|
|
888
936
|
entries: CacheKeyDiff['entries'],
|
|
937
|
+
groups: ReadonlyMap<string, readonly string[]> = new Map(),
|
|
889
938
|
): RootCause[] {
|
|
890
939
|
const roots: RootCause[] = []
|
|
891
940
|
const seen = new Set([taskId])
|
|
892
941
|
const walk = (chain: string[], moved: CacheKeyDiff['entries']): void => {
|
|
893
942
|
for (const e of moved) {
|
|
894
|
-
if (e.kind !== 'upstream'
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
943
|
+
if (e.kind !== 'upstream') continue
|
|
944
|
+
const task = upstreamTask(e.name)
|
|
945
|
+
if (seen.has(task)) continue
|
|
946
|
+
seen.add(task)
|
|
947
|
+
const next = [...chain, ...e.name.split(GROUP_VIA)]
|
|
948
|
+
const d = cacheKeyDiff(db, runId, task)
|
|
949
|
+
// A group: its members carry the walk, named in the key through it
|
|
950
|
+
// (`group ← task`) or, in a row that names the group alone, by the graph.
|
|
951
|
+
if (!d.found) {
|
|
952
|
+
const members = groups.get(task) ?? []
|
|
953
|
+
walk(
|
|
954
|
+
next,
|
|
955
|
+
members.map((name) => ({ ...e, name })),
|
|
956
|
+
)
|
|
957
|
+
continue
|
|
958
|
+
}
|
|
899
959
|
const own = d.entries.filter((x) => x.kind !== 'upstream')
|
|
900
960
|
if (own.length > 0) roots.push({ chain: next, entries: own })
|
|
901
961
|
walk(next, d.entries)
|
package/src/index.ts
CHANGED
|
@@ -16,13 +16,18 @@ export { UserError, isUserError } from './util/index.js'
|
|
|
16
16
|
export { PLUGIN_HOOKS, type PluginHook } from './config.js'
|
|
17
17
|
// The machine as this process may use it — cores and memory capped by the
|
|
18
18
|
// cgroup a container runs under. Core's default worker count reads the
|
|
19
|
-
// first; a policy that budgets memory (`@vzn/vx-
|
|
19
|
+
// first; a policy that budgets memory (`@vzn/vx-history`) reads
|
|
20
20
|
// the second, so the cgroup walk has one implementation.
|
|
21
21
|
export { machineMemoryBytes, machineParallelism } from './util/index.js'
|
|
22
22
|
// How core reads a task glob — its spelling, and which characters are
|
|
23
23
|
// wildcards (`[` `]` and an escape are) — for a plugin that turns declared
|
|
24
24
|
// outputs into something else (`@vzn/vx-reapi`'s literal `output_paths`).
|
|
25
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
isLiteralPattern,
|
|
27
|
+
normalizeGlob,
|
|
28
|
+
outputDirPrefixes,
|
|
29
|
+
restorableOutput,
|
|
30
|
+
} from './util/index.js'
|
|
26
31
|
|
|
27
32
|
// Schema types and helpers (used by user vx.config files and presets).
|
|
28
33
|
export type {
|
|
@@ -43,10 +48,13 @@ export type {
|
|
|
43
48
|
SandboxConfig,
|
|
44
49
|
SandboxGrants,
|
|
45
50
|
SandboxDenials,
|
|
51
|
+
Platform,
|
|
46
52
|
} from './config.js'
|
|
47
|
-
export { defineProject, defineWorkspace } from './config.js'
|
|
53
|
+
export { defineProject, defineWorkspace, PLATFORMS } from './config.js'
|
|
48
54
|
export { definePlugin, refuseUnknownOptions } from './orchestrator/index.js'
|
|
49
55
|
export { branchCacheScope } from './workspace/index.js'
|
|
56
|
+
// A plugin reading the base branch's history: a shallow clone fetches it.
|
|
57
|
+
export { mergeBaseWith } from './workspace/index.js'
|
|
50
58
|
|
|
51
59
|
// Programmatic engine API: run / plan / prepare (docs/cli.md § Programmatic
|
|
52
60
|
// API). The graph primitives, the cache-key hashing seam and the git / host
|
|
@@ -55,7 +63,7 @@ export { branchCacheScope } from './workspace/index.js'
|
|
|
55
63
|
// consumer is re-added when one appears, shaped by its use.
|
|
56
64
|
export { run, planRun, prepareRun } from './orchestrator/index.js'
|
|
57
65
|
// The per-task duration history a `schedule` plugin learns from
|
|
58
|
-
// (`@vzn/vx-
|
|
66
|
+
// (`@vzn/vx-history` does; a plugin package reaches core only
|
|
59
67
|
// through this façade, which is what put these here).
|
|
60
68
|
export { currentBranch } from './orchestrator/index.js'
|
|
61
69
|
export { LocalHistoryProvider } from './history/index.js'
|
|
@@ -75,7 +83,7 @@ export type { Logger, OutputView } from './report/index.js'
|
|
|
75
83
|
// with only `taskId()` to JOIN an id and nothing exported to SPLIT one,
|
|
76
84
|
// consumers roll their own `split('#', 2)` and drift from the graph, which
|
|
77
85
|
// splits on the FIRST '#'.
|
|
78
|
-
export { outputsOverlap, splitTaskId } from './graph/index.js'
|
|
86
|
+
export { criticalPath, outputsOverlap, splitTaskId, taskDefinitions } from './graph/index.js'
|
|
79
87
|
export type { TaskNode, TaskOutcome, TaskStatus } from './graph/index.js'
|
|
80
88
|
|
|
81
89
|
// Cache classes + the layer interface (the `cache` capability's currency).
|
|
@@ -90,9 +98,10 @@ export type {
|
|
|
90
98
|
RunRecord,
|
|
91
99
|
InvocationRecord,
|
|
92
100
|
} from './cache/index.js'
|
|
93
|
-
// A plugin's keychain (`ctx.secret` on `setup` and a remote's `open`),
|
|
94
|
-
//
|
|
95
|
-
|
|
101
|
+
// A plugin's keychain (`ctx.secret` on `setup` and a remote's `open`), the
|
|
102
|
+
// guard that tells a missing secret from a fault, and the names whose values
|
|
103
|
+
// vx masks, for a plugin that must not send one where it is shown.
|
|
104
|
+
export { isSecretUnavailable, secretNamed } from './util/index.js'
|
|
96
105
|
export type {
|
|
97
106
|
PluginSecrets,
|
|
98
107
|
SecretRequest,
|
|
@@ -111,7 +120,7 @@ export { loadProjectConfig } from './workspace/index.js'
|
|
|
111
120
|
export type { ProjectMeta } from './workspace/index.js'
|
|
112
121
|
// The run path's RESOLVED view — plugin `config` and `project` stages
|
|
113
122
|
// applied — for a reader outside the CLI
|
|
114
|
-
// (`@vzn/vx-
|
|
123
|
+
// (`@vzn/vx-history`, an embedder's task catalog). What `vx show`
|
|
115
124
|
// prints.
|
|
116
125
|
export { loadResolvedProjects } from './orchestrator/index.js'
|
|
117
126
|
export type { ProjectEntry } from './workspace/index.js'
|
|
@@ -122,6 +131,7 @@ export type { ProjectEntry } from './workspace/index.js'
|
|
|
122
131
|
export {
|
|
123
132
|
applyMigration,
|
|
124
133
|
foldScriptHooks,
|
|
134
|
+
isStarterConfig,
|
|
125
135
|
PERSISTENT_TODO,
|
|
126
136
|
pruneOrphanPersistentNotes,
|
|
127
137
|
quoteTsLiteral,
|
|
@@ -195,19 +205,26 @@ export type { RunStartInfo } from './report/index.js'
|
|
|
195
205
|
// speaks a wire — and a seam with no consumer is a special case in waiting.)
|
|
196
206
|
// A remote executor gives a task back to the local floor by rejecting with
|
|
197
207
|
// this (B-100): core runs it here and says why once.
|
|
198
|
-
export { executorFallback } from './exec/index.js'
|
|
208
|
+
export { executorFallback, hostPlatform, sampleTrees, type TreeUsage } from './exec/index.js'
|
|
199
209
|
// How the local executor puts the args after `--` on a command, for an
|
|
200
210
|
// executor that runs `ExecuteRequest.command` itself: `@vzn/vx-reapi` and
|
|
201
211
|
// the `vx init --plugin executor` example each joined their own, and a
|
|
202
212
|
// trailing comment or a template literal's closing newline lost the args
|
|
203
213
|
// there while the local floor kept them (X-112).
|
|
204
214
|
export { withForwardArgs } from './exec/index.js'
|
|
215
|
+
// The `npm_execpath` core gives a local task: a `@vzn/vx-dte` worker gives
|
|
216
|
+
// its tasks the same, its own machine's, not what launched it.
|
|
217
|
+
export { packageManagerPath } from './exec/index.js'
|
|
218
|
+
// A task's output both streams in the order written, bounded as core's
|
|
219
|
+
// capture is: what a worker saves through `TaskArtifacts` (`@vzn/vx-dte`).
|
|
220
|
+
export { BoundedCapture, type CapturedChunk } from './exec/index.js'
|
|
205
221
|
// Core's local executor, sandbox included, for a plugin that runs tasks
|
|
206
|
-
// itself: `@vzn/vx-dte`'s orchestrator and its
|
|
222
|
+
// itself: `@vzn/vx-dte`'s orchestrator and its workers.
|
|
207
223
|
export { localFloor, type LocalFloor } from './orchestrator/index.js'
|
|
208
|
-
// A task's sandbox request built where
|
|
224
|
+
// A task's sandbox request built where a worker runs it: the paths and the
|
|
209
225
|
// walls are that machine's (`@vzn/vx-dte`, an orchestrator of another OS).
|
|
210
|
-
|
|
226
|
+
// Its sweep takes back each placeholder the task never wrote, as core does.
|
|
227
|
+
export { sandboxRequestFor, sweepPlaceholders, type SandboxRequest } from './orchestrator/index.js'
|
|
211
228
|
// A runtime probe answered where a `runtime` executor runs the task.
|
|
212
229
|
export { answerRuntime } from './cache/index.js'
|
|
213
230
|
export type {
|
|
@@ -212,12 +212,16 @@ export function admitTasks(
|
|
|
212
212
|
upstream,
|
|
213
213
|
nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
|
|
214
214
|
})
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
// Join a sibling already computing this exact task: wait, then
|
|
216
|
+
// executeTask cache-hits on the artifact it just saved. A sibling that
|
|
217
|
+
// saved nothing (it failed) leaves this task to run, so the joiners
|
|
218
|
+
// released together take the barrier in turn: each returning straight
|
|
219
|
+
// to executeTask ran the task side by side in one directory.
|
|
220
|
+
let joined = false
|
|
221
|
+
for (let existing = inflight.get(hash); existing !== undefined;) {
|
|
222
|
+
joined = true
|
|
219
223
|
await existing.catch(() => {})
|
|
220
|
-
|
|
224
|
+
existing = inflight.get(hash)
|
|
221
225
|
}
|
|
222
226
|
// Become the executor: register a barrier siblings await. get→set has
|
|
223
227
|
// no await between, so registration is atomic — at most one executor
|
|
@@ -229,7 +233,7 @@ export function admitTasks(
|
|
|
229
233
|
release = resolve
|
|
230
234
|
}),
|
|
231
235
|
)
|
|
232
|
-
const execArgs = buildExecuteArgs(node, upstream)
|
|
236
|
+
const execArgs = buildExecuteArgs(node, upstream, !joined)
|
|
233
237
|
try {
|
|
234
238
|
// `return await`, deliberately: the finally must run after the task
|
|
235
239
|
// settles, not when its promise is handed back.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `--affected` per task (
|
|
1
|
+
// `--affected` per task (2026-10-04). The diff seeds the tasks it
|
|
2
2
|
// reaches; a requested task runs when its `dependsOn` closure holds one.
|
|
3
3
|
// So a change reaches another project only along a task edge: `web#test`
|
|
4
4
|
// behind `^build` runs when `ui#build` (or what it depends on) is reached,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
import { applyGraphStage } from './graph-stage.js'
|
|
27
27
|
import { hasHook } from './plugin-host.js'
|
|
28
28
|
import type { VxPlugin } from './plugin.js'
|
|
29
|
-
import { isDefaultBuild } from '
|
|
29
|
+
import { isDefaultBuild } from '../graph/index.js'
|
|
30
30
|
|
|
31
31
|
/** Every file in this set re-keys each of its project's tasks, whatever their inputs. */
|
|
32
32
|
const KEYED_BY_EVERY_TASK: ReadonlySet<string> = new Set(PROJECT_CONFIG_FILENAMES)
|
|
@@ -50,8 +50,10 @@ function wholeProjects(
|
|
|
50
50
|
const keyed = Object.values(projects.get(name)?.config.tasks ?? {}).filter(
|
|
51
51
|
(t) => t.exec !== undefined && t.inputs !== undefined,
|
|
52
52
|
)
|
|
53
|
+
const links = changes.dirLinksIn?.get(name)
|
|
53
54
|
const unclaimed = (rel: string): boolean =>
|
|
54
|
-
KEYED_BY_EVERY_TASK.has(rel) ||
|
|
55
|
+
KEYED_BY_EVERY_TASK.has(rel) ||
|
|
56
|
+
(!byKeys && !keyed.some((t) => declaresInput(t, rel, null, links?.has(rel))))
|
|
55
57
|
const rel = rels.find(unclaimed)
|
|
56
58
|
if (rel !== undefined) whole.set(name, rel)
|
|
57
59
|
}
|
|
@@ -118,7 +120,7 @@ export function affectedRoots(
|
|
|
118
120
|
// it passes through: a holder's own node is already in `deps`.
|
|
119
121
|
const passesChanged = (n: TaskNode): boolean => {
|
|
120
122
|
for (const raw of n.config.dependsOn ?? []) {
|
|
121
|
-
const spec = parseDependencySpec(raw)
|
|
123
|
+
const spec = parseDependencySpec(raw, 'dependsOn')
|
|
122
124
|
if (spec.kind !== 'deps' || spec.negated) continue
|
|
123
125
|
const re = isTaskPattern(spec.task) ? compileTaskPattern(spec.task) : null
|
|
124
126
|
const holds = (name: string): boolean => {
|
|
@@ -147,7 +149,9 @@ export function affectedRoots(
|
|
|
147
149
|
// Asked of every node, not only the changed projects' (the
|
|
148
150
|
// `workspaceFiles` owners): a `graph` hook may have given the glob.
|
|
149
151
|
if (keyed) {
|
|
150
|
-
const file = changes.changed.find((rel) =>
|
|
152
|
+
const file = changes.changed.find((rel) =>
|
|
153
|
+
declaresInput(task, null, rel, changes.dirLinks?.has(rel)),
|
|
154
|
+
)
|
|
151
155
|
const dir =
|
|
152
156
|
file === undefined
|
|
153
157
|
? (changes.nested ?? []).find((d) => workspaceFilesReachInto(task, d))
|
|
@@ -171,7 +175,8 @@ export function affectedRoots(
|
|
|
171
175
|
seeds?.set(n.id, inProject(n.projectName, whole.get(n.projectName)))
|
|
172
176
|
return true
|
|
173
177
|
}
|
|
174
|
-
const
|
|
178
|
+
const links = changes.dirLinksIn?.get(n.projectName)
|
|
179
|
+
const rel = rels.find((r) => declaresInput(task, r, null, links?.has(r)))
|
|
175
180
|
if (rel === undefined) return false
|
|
176
181
|
if (seeds !== null) seeds.set(n.id, { ...inProject(n.projectName, rel), kind: 'input' })
|
|
177
182
|
return true
|
|
@@ -156,6 +156,8 @@ export class DeferredOutputs {
|
|
|
156
156
|
await this.args.cache.save({
|
|
157
157
|
hash: entry.hash,
|
|
158
158
|
projectDir: producer.projectDir,
|
|
159
|
+
// As saveMiss: a link is bounded by the workspace, not the project.
|
|
160
|
+
workspaceRoot: this.args.workspaceRoot,
|
|
159
161
|
outputFiles,
|
|
160
162
|
...(emptyDirs.length > 0 ? { outputDirs: emptyDirs } : {}),
|
|
161
163
|
entry: entry.entry,
|