@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.
Files changed (126) hide show
  1. package/docs/caching.md +84 -28
  2. package/docs/cli.md +273 -408
  3. package/docs/execution.md +19 -44
  4. package/docs/features.md +13 -15
  5. package/docs/patterns.md +5 -7
  6. package/docs/schema.md +68 -61
  7. package/package.json +5 -5
  8. package/schemas/last.json +7 -4
  9. package/schemas/plan.json +2 -2
  10. package/skills/vx/SKILL.md +19 -6
  11. package/src/bin.ts +13 -7
  12. package/src/cache/cache.ts +60 -11
  13. package/src/cache/git-inputs.ts +6 -0
  14. package/src/cache/index.ts +10 -2
  15. package/src/cache/inputs.ts +0 -24
  16. package/src/cache/layer.ts +32 -12
  17. package/src/cache/layered-cache.ts +112 -315
  18. package/src/cache/plugin-cache.ts +240 -0
  19. package/src/cache/policy.ts +9 -0
  20. package/src/cache/remote-lane.ts +383 -0
  21. package/src/cache/run-history.ts +92 -8
  22. package/src/cache/run-store.ts +77 -0
  23. package/src/cache/schema.ts +44 -13
  24. package/src/cache/scoped-cache.ts +22 -2
  25. package/src/cli/cache.ts +33 -16
  26. package/src/cli/completions.ts +11 -7
  27. package/src/cli/foreign-flags.ts +10 -22
  28. package/src/cli/help.ts +22 -38
  29. package/src/cli/index.ts +25 -11
  30. package/src/cli/info.ts +8 -6
  31. package/src/cli/init.ts +14 -15
  32. package/src/cli/last.ts +20 -10
  33. package/src/cli/lock.ts +39 -27
  34. package/src/cli/plan-format.ts +10 -61
  35. package/src/cli/plugin-commands.ts +77 -6
  36. package/src/cli/plugin-templates.ts +1 -1
  37. package/src/cli/run.ts +31 -191
  38. package/src/cli/select.ts +18 -9
  39. package/src/cli/show.ts +72 -40
  40. package/src/cli/task-verb.ts +4 -4
  41. package/src/cli/upgrade.ts +132 -39
  42. package/src/cli/watch-cycle.ts +2 -1
  43. package/src/cli/watch-filter.ts +7 -2
  44. package/src/cli/watch-judge.ts +6 -1
  45. package/src/cli/watch.ts +73 -34
  46. package/src/cli/why.ts +14 -5
  47. package/src/cli/workspace-config.ts +2 -2
  48. package/src/config.ts +0 -6
  49. package/src/exec/executor.ts +1 -1
  50. package/src/exec/sandbox-runtime.ts +1 -1
  51. package/src/exec/sandbox-violations.ts +3 -1
  52. package/src/graph/index.ts +9 -1
  53. package/src/graph/priorities.ts +1 -1
  54. package/src/graph/scheduler.ts +12 -16
  55. package/src/graph/task-graph.ts +119 -21
  56. package/src/graph/task-status.ts +95 -0
  57. package/src/{orchestrator → history}/failure-mode.ts +20 -11
  58. package/src/{orchestrator → history}/history.ts +61 -5
  59. package/src/history/index.ts +48 -0
  60. package/src/{orchestrator → history}/metrics.ts +14 -23
  61. package/src/{orchestrator → history}/miss-reason.ts +1 -1
  62. package/src/index.ts +21 -29
  63. package/src/orchestrator/affected-tasks.ts +8 -3
  64. package/src/orchestrator/doctor.ts +2 -2
  65. package/src/orchestrator/execute-args.ts +80 -0
  66. package/src/orchestrator/execute-task.ts +47 -429
  67. package/src/orchestrator/fingerprint-watch.ts +2 -2
  68. package/src/orchestrator/index.ts +3 -74
  69. package/src/orchestrator/miss-save.ts +33 -3
  70. package/src/orchestrator/options.ts +1 -36
  71. package/src/orchestrator/output-host.ts +4 -3
  72. package/src/orchestrator/persistent-task.ts +321 -0
  73. package/src/orchestrator/persistent.ts +130 -3
  74. package/src/orchestrator/placement.ts +3 -3
  75. package/src/orchestrator/plan.ts +18 -5
  76. package/src/orchestrator/plugin-host.ts +77 -47
  77. package/src/orchestrator/plugin.ts +86 -14
  78. package/src/orchestrator/prepare.ts +74 -31
  79. package/src/orchestrator/projects.ts +3 -0
  80. package/src/orchestrator/remote-prefetch.ts +23 -6
  81. package/src/orchestrator/run-artifacts.ts +5 -177
  82. package/src/orchestrator/run-context.ts +55 -0
  83. package/src/orchestrator/run-failures.ts +15 -3
  84. package/src/orchestrator/run-records.ts +36 -4
  85. package/src/orchestrator/run-report.ts +81 -179
  86. package/src/orchestrator/run-settings.ts +1 -16
  87. package/src/orchestrator/run-stop.ts +153 -0
  88. package/src/orchestrator/run.ts +313 -555
  89. package/src/orchestrator/sandbox-fix.ts +0 -16
  90. package/src/orchestrator/signals.ts +14 -6
  91. package/src/orchestrator/stable-keys.ts +107 -3
  92. package/src/orchestrator/steps.ts +50 -0
  93. package/src/orchestrator/task-hash.ts +3 -16
  94. package/src/orchestrator/task-log-buffer.ts +1 -1
  95. package/src/orchestrator/task-process.ts +94 -0
  96. package/src/orchestrator/telemetry-host.ts +1 -1
  97. package/src/orchestrator/telemetry.ts +6 -83
  98. package/src/{orchestrator → report}/events.ts +41 -8
  99. package/src/{orchestrator → report}/forecast.ts +35 -2
  100. package/src/{orchestrator → report}/framed-output.ts +39 -34
  101. package/src/report/index.ts +36 -0
  102. package/src/{orchestrator → report}/logger.ts +166 -21
  103. package/src/{orchestrator → report}/status-line.ts +84 -7
  104. package/src/{orchestrator → report}/summary.ts +179 -63
  105. package/src/report/tally.ts +83 -0
  106. package/src/settings.ts +13 -23
  107. package/src/{orchestrator → util}/colors.ts +49 -4
  108. package/src/util/config-frame.ts +31 -2
  109. package/src/util/errors.ts +32 -0
  110. package/src/util/git-ignored.ts +143 -0
  111. package/src/util/index.ts +25 -3
  112. package/src/util/refusal-notice.ts +111 -0
  113. package/src/util/report.ts +39 -0
  114. package/src/util/settle.ts +14 -39
  115. package/src/util/tail.ts +1 -1
  116. package/src/util/ui.ts +197 -0
  117. package/src/workspace/affected.ts +20 -23
  118. package/src/workspace/dependency-rules.ts +12 -4
  119. package/src/workspace/migration.ts +87 -91
  120. package/src/workspace/workspace.ts +5 -4
  121. package/schemas/summary.json +0 -133
  122. package/src/cache/chained-cache.ts +0 -276
  123. package/src/orchestrator/tally.ts +0 -112
  124. /package/src/{orchestrator → report}/path-links.ts +0 -0
  125. /package/src/{orchestrator → report}/plain-output.ts +0 -0
  126. /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
- process.stderr.write(`vx watch: ${what}: ${err instanceof Error ? err.message : String(err)}\n`)
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 || parsed.graph !== undefined) {
65
- return 'vx watch: --dry / --graph are not supported in watch mode'
64
+ if (parsed.dry !== undefined) {
65
+ return 'vx watch: --dry is not supported in watch mode'
66
66
  }
67
- if (parsed.summarize !== undefined || parsed.profile !== undefined) {
68
- return 'vx watch: --summarize / --profile are not supported in watch mode (would overwrite per cycle)'
67
+ if (parsed.profile !== undefined) {
68
+ return 'vx watch: --profile is not supported in watch mode (would overwrite per cycle)'
69
69
  }
70
- // All three format ONE run's result and are consumed by `runCmd` alone, so
71
- // a watch loop silently ignored them. `--verbosity 0` is not rejected: it
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
- parsed.report !== undefined ||
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
- process.stderr.write(`vx watch: ${parsed.error}${seeHelp('watch')}\n`)
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
- process.stderr.write(`${refused}\n`)
88
+ sayLine('error', refused, 'vx watch')
94
89
  return 1
95
90
  }
96
91
  if (parsed.tasks.length === 0) {
97
- process.stderr.write(
98
- `vx watch: missing task name (vx watch <task>, e.g. vx watch build)${seeHelp('watch')}\n`,
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
- process.stderr.write(`vx watch: ${resolved.error}\n`)
103
+ sayNotice('error', 'vx watch', resolved.error)
107
104
  return 1
108
105
  }
109
106
  if ('nothingSelected' in resolved) {
110
- process.stderr.write(`vx watch: ${resolved.nothingSelected}\n`)
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) process.stderr.write(`vx watch: ${run.refused}\n`)
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
- process.stderr.write(
231
- `vx watch: cycle failed: ${err instanceof Error ? err.message : String(err)}\n`,
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
- process.stderr.write(`vx watch: no projects in scope\n`)
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) process.stderr.write(`vx watch: ${cycle.refused}\n`)
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
- process.stderr.write(`vx watch: cycle failed: ${message}\n`)
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
- trigger(`root ${filename}`, abs)
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 is armed, and again after a cycle that re-read the
801
- // watched set (a member came or went, the lock or a plugin changed).
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} task${lastTasks === 1 ? '' : 's'}`
805
- process.stdout.write(`watching ${count} project${count === 1 ? '' : 's'}${tasks}\n`)
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) sayWatching(next.projects.length)
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
- explainCacheKeyQuery as explainCacheKey,
12
+ explainCacheKey,
13
13
  inputWriters,
14
14
  latestRunId,
15
15
  rootCauses,
16
- whyDidThisRerunQuery as whyDidThisRerun,
17
- resolveRunId,
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
- process.stderr.write(`${message}\n`)
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
@@ -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 (`VX_RUN_KILL_GRACE`, 2 s) after the abort, then
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
- return [...paths]
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
  /**
@@ -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,
@@ -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: Map<string, TaskNode>): Map<string, number> {
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;
@@ -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); `--summarize` rows, the event
83
- * stream and the summary footer show it.
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, `--summarize`, the telemetry record and the run row all
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