@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.
Files changed (184) hide show
  1. package/README.md +29 -22
  2. package/launcher.cjs +9 -2
  3. package/launcher.sh +2 -2
  4. package/package.json +5 -5
  5. package/schemas/cache.json +5 -5
  6. package/schemas/error.json +1 -1
  7. package/schemas/info.json +16 -2
  8. package/schemas/last.json +112 -5
  9. package/schemas/plan.json +28 -2
  10. package/schemas/show.json +10 -1
  11. package/skills/vx/SKILL.md +18 -2
  12. package/src/bin.ts +53 -12
  13. package/src/cache/archive.ts +324 -23
  14. package/src/cache/cache.ts +268 -53
  15. package/src/cache/git-inputs.ts +219 -87
  16. package/src/cache/index.ts +3 -2
  17. package/src/cache/inputs.ts +460 -78
  18. package/src/cache/key-fold.ts +15 -6
  19. package/src/cache/layer.ts +49 -9
  20. package/src/cache/layered-cache.ts +123 -58
  21. package/src/cache/linked-inputs.ts +1 -1
  22. package/src/cache/output-index.ts +19 -3
  23. package/src/cache/plugin-cache.ts +49 -23
  24. package/src/cache/remote-hits.ts +72 -0
  25. package/src/cache/remote-lane.ts +90 -14
  26. package/src/cache/run-history.ts +70 -23
  27. package/src/cache/run-store.ts +11 -10
  28. package/src/cache/schema.ts +25 -6
  29. package/src/cache/tar-stream.ts +5 -0
  30. package/src/cache/umask.ts +1 -2
  31. package/src/cli/cache.ts +58 -27
  32. package/src/cli/completions.ts +12 -5
  33. package/src/cli/docs.ts +42 -16
  34. package/src/cli/help.ts +58 -19
  35. package/src/cli/index.ts +79 -33
  36. package/src/cli/info.ts +12 -6
  37. package/src/cli/init.ts +186 -31
  38. package/src/cli/last.ts +439 -45
  39. package/src/cli/lock.ts +197 -52
  40. package/src/cli/plan-format.ts +91 -49
  41. package/src/cli/plugin-commands.ts +70 -14
  42. package/src/cli/plugin-templates.ts +13 -13
  43. package/src/cli/registry.ts +209 -0
  44. package/src/cli/run.ts +109 -34
  45. package/src/cli/select.ts +143 -46
  46. package/src/cli/show.ts +59 -16
  47. package/src/cli/task-verb.ts +16 -6
  48. package/src/cli/upgrade.ts +151 -49
  49. package/src/cli/watch-cycle.ts +10 -5
  50. package/src/cli/watch-filter.ts +71 -3
  51. package/src/cli/watch-fs.ts +22 -6
  52. package/src/cli/watch-judge.ts +48 -6
  53. package/src/cli/watch.ts +163 -42
  54. package/src/cli/why.ts +211 -73
  55. package/src/cli/workspace-config.ts +54 -15
  56. package/src/config.ts +13 -1
  57. package/src/exec/env.ts +7 -2
  58. package/src/exec/executor.ts +154 -10
  59. package/src/exec/index.ts +6 -2
  60. package/src/exec/kill-tree.ts +15 -0
  61. package/src/exec/local-executor.ts +1 -1
  62. package/src/exec/proc-sample.ts +3 -1
  63. package/src/exec/runner.ts +253 -54
  64. package/src/exec/sandbox-binds.ts +7 -3
  65. package/src/exec/sandbox-paths.ts +1 -1
  66. package/src/exec/sandbox-runtime.ts +211 -24
  67. package/src/exec/sandbox-violations.ts +52 -20
  68. package/src/graph/critical-path.ts +79 -0
  69. package/src/graph/default-build.ts +19 -0
  70. package/src/graph/definition.ts +38 -0
  71. package/src/graph/folds.ts +38 -5
  72. package/src/graph/index.ts +4 -1
  73. package/src/graph/priorities.ts +74 -8
  74. package/src/graph/scheduler.ts +270 -60
  75. package/src/graph/task-graph.ts +228 -35
  76. package/src/handoff.ts +97 -9
  77. package/src/history/failure-mode.ts +6 -3
  78. package/src/history/history.ts +167 -61
  79. package/src/history/index.ts +2 -0
  80. package/src/history/metrics.ts +90 -30
  81. package/src/index.ts +30 -13
  82. package/src/orchestrator/admission.ts +10 -6
  83. package/src/orchestrator/affected-tasks.ts +11 -6
  84. package/src/orchestrator/deferred-outputs.ts +2 -0
  85. package/src/orchestrator/doctor.ts +134 -42
  86. package/src/orchestrator/execute-args.ts +4 -1
  87. package/src/orchestrator/execute-task.ts +130 -38
  88. package/src/orchestrator/fingerprint-watch.ts +1 -1
  89. package/src/orchestrator/graph-memo.ts +140 -0
  90. package/src/orchestrator/graph-stage.ts +18 -1
  91. package/src/orchestrator/held-servers.ts +1 -1
  92. package/src/orchestrator/hit-restore.ts +217 -119
  93. package/src/orchestrator/index.ts +4 -3
  94. package/src/orchestrator/local-floor.ts +119 -25
  95. package/src/orchestrator/local-shortcircuit.ts +28 -4
  96. package/src/orchestrator/lockfile-claim.ts +96 -16
  97. package/src/orchestrator/miss-save.ts +42 -8
  98. package/src/orchestrator/options.ts +8 -6
  99. package/src/orchestrator/output-host.ts +43 -14
  100. package/src/orchestrator/output-log.ts +90 -10
  101. package/src/orchestrator/persistent-task.ts +84 -29
  102. package/src/orchestrator/persistent.ts +16 -4
  103. package/src/orchestrator/placement.ts +185 -17
  104. package/src/orchestrator/plan.ts +64 -7
  105. package/src/orchestrator/plugin-host.ts +214 -42
  106. package/src/orchestrator/plugin.ts +113 -37
  107. package/src/orchestrator/prepare.ts +168 -30
  108. package/src/orchestrator/projects.ts +65 -40
  109. package/src/orchestrator/remote-prefetch.ts +119 -25
  110. package/src/orchestrator/run-artifacts.ts +95 -32
  111. package/src/orchestrator/run-context.ts +39 -41
  112. package/src/orchestrator/run-failures.ts +65 -23
  113. package/src/orchestrator/run-id.ts +6 -3
  114. package/src/orchestrator/run-keys.ts +149 -13
  115. package/src/orchestrator/run-live.ts +294 -0
  116. package/src/orchestrator/run-lock.ts +13 -7
  117. package/src/orchestrator/run-records.ts +35 -29
  118. package/src/orchestrator/run-report.ts +3 -3
  119. package/src/orchestrator/run-steps.ts +58 -22
  120. package/src/orchestrator/run-stop.ts +22 -5
  121. package/src/orchestrator/run.ts +331 -88
  122. package/src/orchestrator/sandbox-fix.ts +7 -5
  123. package/src/orchestrator/sandbox-request.ts +70 -20
  124. package/src/orchestrator/shell-verdict.ts +5 -5
  125. package/src/orchestrator/signals.ts +26 -1
  126. package/src/orchestrator/stable-keys.ts +92 -8
  127. package/src/orchestrator/steps.ts +2 -2
  128. package/src/orchestrator/task-artifacts.ts +90 -26
  129. package/src/orchestrator/task-hash.ts +78 -13
  130. package/src/orchestrator/task-log-buffer.ts +72 -36
  131. package/src/orchestrator/task-process.ts +7 -1
  132. package/src/orchestrator/telemetry-host.ts +22 -2
  133. package/src/orchestrator/telemetry.ts +53 -21
  134. package/src/orchestrator/upstream.ts +15 -0
  135. package/src/report/events.ts +32 -13
  136. package/src/report/forecast.ts +31 -9
  137. package/src/report/framed-output.ts +141 -50
  138. package/src/report/index.ts +1 -0
  139. package/src/report/logger.ts +170 -179
  140. package/src/report/path-links.ts +12 -4
  141. package/src/report/plain-output.ts +13 -5
  142. package/src/report/status-line.ts +81 -53
  143. package/src/report/summary.ts +114 -32
  144. package/src/report/terminal-signals.ts +13 -2
  145. package/src/settings.ts +7 -6
  146. package/src/util/bun-version.ts +4 -1
  147. package/src/util/colors.ts +4 -2
  148. package/src/util/config-frame.ts +41 -4
  149. package/src/{graph → util}/dependency-spec.ts +22 -13
  150. package/src/util/edit-distance.ts +13 -1
  151. package/src/util/env-flag.ts +9 -0
  152. package/src/util/errors.ts +59 -3
  153. package/src/util/essential-env.ts +57 -0
  154. package/src/util/git-ignored.ts +1 -1
  155. package/src/util/hash.ts +35 -0
  156. package/src/util/hide-env.ts +45 -5
  157. package/src/util/index.ts +21 -5
  158. package/src/util/num.ts +8 -0
  159. package/src/util/paths.ts +91 -21
  160. package/src/util/report.ts +6 -1
  161. package/src/util/secret-mask.ts +62 -1
  162. package/src/util/secrets.ts +7 -2
  163. package/src/util/size.ts +11 -3
  164. package/src/util/tail.ts +6 -1
  165. package/src/util/timing.ts +13 -1
  166. package/src/util/ui.ts +61 -3
  167. package/src/util/which.ts +1 -1
  168. package/src/util/write-file.ts +67 -0
  169. package/src/workspace/affected.ts +710 -107
  170. package/src/workspace/bracket-literal.ts +84 -0
  171. package/src/workspace/canonical-dir.ts +37 -0
  172. package/src/workspace/config-bytecode.ts +17 -11
  173. package/src/workspace/config-eval.ts +29 -8
  174. package/src/workspace/config-imports.ts +199 -58
  175. package/src/workspace/config-schema.ts +242 -39
  176. package/src/workspace/filter.ts +37 -8
  177. package/src/workspace/index.ts +11 -1
  178. package/src/workspace/json-data.ts +3 -4
  179. package/src/workspace/lockfile.ts +57 -8
  180. package/src/workspace/migration.ts +69 -18
  181. package/src/workspace/package-graph.ts +5 -4
  182. package/src/workspace/project-loader.ts +226 -37
  183. package/src/workspace/repo-id.ts +1 -1
  184. package/src/workspace/workspace.ts +176 -33
@@ -13,9 +13,10 @@
13
13
  // process primitives — the module-boundary matrix forbids `exec` → `cache`,
14
14
  // which is why sandbox baselines arrive pre-resolved on the request.
15
15
 
16
+ import type { SandboxConfig } from '../config.js'
16
17
  import type { CaptureConfig, RunResult } from './runner.js'
17
18
  import type { ResolvedSandboxConfig, SandboxViolation } from './sandbox-runtime.js'
18
- import { UserError, type ui } from '../util/index.js'
19
+ import { thrownMessage, UserError, type ui } from '../util/index.js'
19
20
 
20
21
  /** Sandbox baselines + the user's resolved sandbox block, when the task is sandboxed. */
21
22
  export interface ExecuteSandbox {
@@ -94,6 +95,12 @@ export interface ServerSpec {
94
95
  readonly readyWhen?: string
95
96
  /** Bounds the readiness wait. */
96
97
  readonly timeoutMs?: number
98
+ /**
99
+ * Its `exec.sandbox`, and the directories of the projects nested in its
100
+ * own (a wall its grants do not reach): it starts inside that sandbox,
101
+ * built where it runs, as a one-shot task's.
102
+ */
103
+ readonly sandbox?: { readonly config: SandboxConfig; readonly nested: readonly string[] }
97
104
  }
98
105
 
99
106
  export interface ExecuteRequest {
@@ -109,6 +116,15 @@ export interface ExecuteRequest {
109
116
  * this machine — needs a stable address, and the key is that address.
110
117
  */
111
118
  readonly cacheKey?: string
119
+ /**
120
+ * Core saves no entry under `cacheKey` after this attempt: the run's
121
+ * policy writes no layer (`--no-cache`, `--cache-policy local:r`), or an
122
+ * upstream failed under `--continue=always`, ran unkeyed or is keyless.
123
+ * An executor that saves a task's result for its dependants keeps it off
124
+ * that key too: a distributed run's worker saved it, and the next run
125
+ * hit an entry the user had said not to write (2026-10-24).
126
+ */
127
+ readonly unsaved?: boolean
112
128
  /**
113
129
  * Cache READS are disabled for this run (`--force` / `--no-cache`). An
114
130
  * executor that keeps its OWN remote record of executions must not serve
@@ -123,6 +139,11 @@ export interface ExecuteRequest {
123
139
  * execution under the same key.
124
140
  */
125
141
  readonly remoteOnly?: boolean
142
+ /**
143
+ * The platform to run it on, one of the task's `exec.platforms` and the
144
+ * executor's `platforms`: its key includes it. Absent when the task names none.
145
+ */
146
+ readonly platform?: string
126
147
  /**
127
148
  * Whether this task's outputs must land on the submitter's disk now.
128
149
  * 'eager' (or absent) = materialise as today. 'deferred' = leave them
@@ -198,9 +219,17 @@ export interface ExecuteResult extends RunResult {
198
219
  * existed. `deferred` means they are NOT on disk and `materialize()`
199
220
  * fetches them; core calls it lazily, at most once, and then saves an
200
221
  * ordinary cache entry, so a deferred task leaves no permanent third
201
- * state behind.
222
+ * state behind. An executor may answer `deferred` to an `eager` request
223
+ * too, when it cannot bring them here: core saves nothing from this disk.
202
224
  */
203
225
  readonly outputs?: { kind: 'disk' } | { kind: 'deferred'; materialize: () => Promise<void> }
226
+ /**
227
+ * Tracked files the command changed where it ran, workspace-relative
228
+ * POSIX: an executor that runs it elsewhere says so, as this disk shows
229
+ * none of it. One the key read withholds the save under the key, as an
230
+ * input changed here does.
231
+ */
232
+ readonly changed?: readonly string[]
204
233
  /**
205
234
  * Executor-reported placement label — which machine ran the command
206
235
  * (a REAPI worker id, a pool member). Absent = this host. Rides
@@ -214,7 +243,16 @@ export interface ExecuteResult extends RunResult {
214
243
  * them: the run history keeps them, so a task that used all of one
215
244
  * reads as wanting a bigger one (`TaskResources.saturated*`).
216
245
  */
217
- readonly machine?: { readonly cores: number; readonly memoryBytes: number }
246
+ readonly machine?: {
247
+ readonly cores: number
248
+ readonly memoryBytes: number
249
+ /**
250
+ * The share of a core each runnable thread got there over the task's
251
+ * run (1 when a core was free). On a crowded machine the CPU a task
252
+ * used is its share, not its need: the run history divides by this.
253
+ */
254
+ readonly cpuShare?: number
255
+ }
218
256
  }
219
257
 
220
258
  /** What an executor sees when a task is PLACED — once per task, before scheduling. */
@@ -251,7 +289,7 @@ export interface TaskExecutor {
251
289
  * it runs the task (`ExecuteRequest.servers`). It is offered the
252
290
  * dependants of a persistent task with no `exec.sandbox` and no
253
291
  * `exec.remote: false`; that task then runs here only for a dependant
254
- * placed here (owner, 2026-10-22: nothing runs on a distributed run's
292
+ * placed here (2026-10-22: nothing runs on a distributed run's
255
293
  * orchestrator).
256
294
  */
257
295
  readonly servers?: boolean
@@ -263,6 +301,31 @@ export interface TaskExecutor {
263
301
  * here: their key holds this machine's answer (C-2).
264
302
  */
265
303
  readonly runtime?: boolean
304
+ /**
305
+ * A `remote` executor that runs every task of the run, so none runs on
306
+ * this machine; only cache hits restore here (2026-10-24: "no
307
+ * task should ever run on the orchestrator"). Nothing is pinned for it:
308
+ * it is offered `exec.remote: false` and `exec.interactive` tasks too,
309
+ * and starts every server a dependant needs. A task it cannot take (a
310
+ * platform it does not offer, one it declines, a server requested on its
311
+ * own) refuses the run, and a task it gives back fails.
312
+ */
313
+ readonly exclusive?: boolean
314
+ /**
315
+ * A `remote` executor that enforces `ExecuteRequest.timeoutMs` where it
316
+ * runs the command, timed from its start, and reports `timedOut`. Core
317
+ * then arms no clock of its own: armed at the request, it counted the
318
+ * wait for a worker still booting and killed a 1 s task with a 3 s
319
+ * timeout before it began (2026-10-24). The run's stop still aborts.
320
+ */
321
+ readonly timeouts?: boolean
322
+ /**
323
+ * The platforms this executor runs tasks on (`exec.platforms`' names). It
324
+ * is offered a task that declares `exec.platforms` only when one of them
325
+ * is listed, and runs it on `ExecuteRequest.platform`; an executor that
326
+ * declares none is never offered such a task off this machine.
327
+ */
328
+ readonly platforms?: readonly string[]
266
329
  /**
267
330
  * How many tasks this executor runs at once. Its tasks then occupy a pool
268
331
  * of this size instead of the local worker slots, so a remote pool is not
@@ -273,9 +336,10 @@ export interface TaskExecutor {
273
336
  /** Per-task opt-out at placement time. Absent = accepts every task it is offered. */
274
337
  accepts?(task: TaskPlacement): boolean
275
338
  /**
276
- * The tasks placed here that have NOT finished yet — called once after
277
- * placement with the whole set, then again after each completion (any
278
- * outcome: a cache hit and a failure both remove one).
339
+ * The tasks placed here that will run and have NOT finished yet — called
340
+ * first once the run's cache has said which placed tasks it holds (those
341
+ * restore, so they are left out), then again after each completion (any
342
+ * outcome, a failure too) and when a task it left out executes after all.
279
343
  *
280
344
  * An executor that provisions something per task — a container, an
281
345
  * allocation, a pod — otherwise has to guess. `capacity` says how much it
@@ -284,8 +348,14 @@ export interface TaskExecutor {
284
348
  * moment the run can no longer use it, rather than holding it until
285
349
  * teardown. Absent = the executor does not care and nothing is computed
286
350
  * for it.
351
+ *
352
+ * `missed`: of `remaining`, those whose keys the run's up-front probe
353
+ * found in no store (each store asked answered for every key), or, when
354
+ * the remote has no batch probe, those the index keeps no hit of: they
355
+ * will run. The rest may still restore, or were not probed (a key an upstream's outputs can
356
+ * change). The same set each call, changed in place.
287
357
  */
288
- demand?(remaining: ReadonlySet<string>): void
358
+ demand?(remaining: ReadonlySet<string>, missed: ReadonlySet<string>): void
289
359
  /**
290
360
  * Rows the run's footer shows for this executor, `[label, text]`, the
291
361
  * text plain or a `ui` inline (a tone colors it): what only it knows, such as the machines it ran tasks on. Asked on every
@@ -299,6 +369,20 @@ export interface TaskExecutor {
299
369
  * sink is on. A throw reports none.
300
370
  */
301
371
  machines?(): readonly MachineReport[]
372
+ /**
373
+ * The run's end had it placed its tasks perfectly, from the run's
374
+ * start: what each took, replayed on the machines it could have had, for
375
+ * the run summary beside the real time. Asked once at the end, only when a sink is on. A throw, or
376
+ * anything but a finite number, reports none.
377
+ */
378
+ idealMs?(): number | undefined
379
+ /**
380
+ * Its own Chrome-trace events for `vx run --profile` (the machines it ran
381
+ * tasks on, in time), asked once at the end, only when the flag is set.
382
+ * `ts` is in microseconds of `performance.now()`; `pid` from 1, which
383
+ * the run moves past its own process. A throw adds none.
384
+ */
385
+ profile?(): readonly Record<string, unknown>[]
302
386
  /**
303
387
  * Where a task it was handed runs (`small#2`), for the live row; asked
304
388
  * per frame. `null` while it holds the task for a machine to begin it:
@@ -318,8 +402,9 @@ export interface MachineReport {
318
402
  memoryBytes: number
319
403
  done: number
320
404
  failed: number
405
+ /** From when it joined to when it left or the run ended: warm-up not counted. */
321
406
  onlineMs: number
322
- /** Time with at least one task running. */
407
+ /** Time with at least one task on it, from its send to its result. */
323
408
  busyMs: number
324
409
  /** Cores in use at once, at the most and on average over its time online. */
325
410
  cpuPeak: number
@@ -329,6 +414,12 @@ export interface MachineReport {
329
414
  memoryAvgBytes: number
330
415
  /** Core-time its tasks used over the core-time it had, 0 to 1. */
331
416
  utilization: number
417
+ /** Cores the executor booked for the tasks it ran, on average over its time online. */
418
+ bookedAvg?: number
419
+ /** From when the executor asked for it to when it joined, for one it started. */
420
+ warmUpMs?: number
421
+ /** When it was asked for (else when it joined), from the executor's start. */
422
+ startMs?: number
332
423
  }
333
424
 
334
425
  /**
@@ -365,6 +456,8 @@ export function assertExecuteResult(
365
456
  `executor '${executor}' returned an invalid result for ${taskId}: ${what} — a plugin bug, not a task failure`,
366
457
  )
367
458
  }
459
+ const shown = (v: unknown): string =>
460
+ typeof v === 'object' && v !== null ? 'an object' : String(v as string)
368
461
  if (typeof res !== 'object' || res === null)
369
462
  fail(`${res === null ? 'null' : typeof res} (expected an object)`)
370
463
  const r = res as Record<string, unknown>
@@ -382,6 +475,46 @@ export function assertExecuteResult(
382
475
  }
383
476
  if (!Array.isArray(r['violations']))
384
477
  fail(`violations is ${typeof r['violations']} (expected an array)`)
478
+ // The optional fields reach the task's row, telemetry and the run
479
+ // history: a `where` object lost the whole run's history to a bind error
480
+ // naming no executor, and a `null` violation failed the task as a TypeError.
481
+ for (const [i, v] of (r['violations'] as unknown[]).entries()) {
482
+ if (typeof v !== 'object' || v === null || typeof (v as { line?: unknown }).line !== 'string')
483
+ fail(`violations[${i}] is not an object with a string line`)
484
+ }
485
+ const where = r['where']
486
+ if (where !== undefined && (typeof where !== 'string' || where === ''))
487
+ fail(`where is ${where === '' ? 'empty' : typeof where} (expected a non-empty string)`)
488
+ for (const key of ['cpuMs', 'peakRssBytes']) {
489
+ const n = r[key]
490
+ if (n !== undefined && !(typeof n === 'number' && Number.isFinite(n) && n >= 0))
491
+ fail(`${key} is ${shown(n)} (expected a non-negative number)`)
492
+ }
493
+ const signal = r['signal']
494
+ if (
495
+ signal !== undefined &&
496
+ typeof signal !== 'string' &&
497
+ !(typeof signal === 'number' && Number.isInteger(signal) && signal > 0)
498
+ )
499
+ fail(`signal is ${shown(signal)} (expected a name or a signal number)`)
500
+ if (r['timedOut'] !== undefined && typeof r['timedOut'] !== 'boolean')
501
+ fail(`timedOut is ${typeof r['timedOut']} (expected a boolean)`)
502
+ const machine = r['machine']
503
+ if (machine !== undefined) {
504
+ if (typeof machine !== 'object' || machine === null) fail('machine is not an object')
505
+ const m = machine as Record<string, unknown>
506
+ // A worker that cannot read its memory reports 0 (vx-dte's join takes it).
507
+ for (const [key, least] of [
508
+ ['cores', 'positive'],
509
+ ['memoryBytes', 'non-negative'],
510
+ ['cpuShare', 'positive'],
511
+ ] as const) {
512
+ const n = m[key]
513
+ if (key === 'cpuShare' && n === undefined) continue
514
+ if (!(typeof n === 'number' && Number.isFinite(n) && (least === 'positive' ? n > 0 : n >= 0)))
515
+ fail(`machine.${key} is ${shown(n)} (expected a ${least} number)`)
516
+ }
517
+ }
385
518
  const outputs = r['outputs'] as { kind?: unknown; materialize?: unknown } | undefined
386
519
  if (outputs !== undefined) {
387
520
  if (typeof outputs !== 'object' || outputs === null) fail('outputs is not an object')
@@ -391,6 +524,12 @@ export function assertExecuteResult(
391
524
  } else if (outputs.kind !== 'disk')
392
525
  fail(`outputs.kind is ${String(outputs.kind)} (expected disk or deferred)`)
393
526
  }
527
+ const changed = r['changed']
528
+ if (
529
+ changed !== undefined &&
530
+ (!Array.isArray(changed) || changed.some((f) => typeof f !== 'string'))
531
+ )
532
+ fail('changed is not an array of paths')
394
533
  }
395
534
 
396
535
  /**
@@ -424,7 +563,7 @@ export function selectExecutor(
424
563
  throw new Error('returned a Promise; accepts is synchronous')
425
564
  }
426
565
  } catch (err) {
427
- const m = err instanceof Error ? err.message : String(err)
566
+ const m = thrownMessage(err)
428
567
  throw new UserError(`${label(executor)} failed in accepts for ${task.taskId}: ${m}`)
429
568
  }
430
569
  if (taken) return executor
@@ -433,3 +572,8 @@ export function selectExecutor(
433
572
  `no executor accepted ${task.taskId} (declared: ${executors.map((e) => e.name).join(', ')}). Core's local executor accepts every task, so this means it is missing from the list.`,
434
573
  )
435
574
  }
575
+
576
+ /** This machine as `exec.platforms` names it: `os-arch` as Bun reports them, `linux-x64`. */
577
+ export function hostPlatform(): string {
578
+ return `${process.platform}-${process.arch}`
579
+ }
package/src/exec/index.ts CHANGED
@@ -9,7 +9,9 @@ export {
9
9
  VX_RUN_WORKSPACE_ENV,
10
10
  } from './env.js'
11
11
  export {
12
+ lineEnds,
12
13
  runCommand,
14
+ restoreTerminalModes,
13
15
  runPersistent,
14
16
  PersistentReadyError,
15
17
  withForwardArgs,
@@ -19,6 +21,7 @@ export {
19
21
  type CaptureConfig,
20
22
  execWord,
21
23
  maskCaptured,
24
+ isDroppedOutputChunk,
22
25
  BoundedCapture,
23
26
  type CapturedChunk,
24
27
  } from './runner.js'
@@ -48,11 +51,12 @@ export {
48
51
  } from './sandbox-paths.js'
49
52
  export { bindableWrites, punchWalls } from './sandbox-binds.js'
50
53
  export { isLocalExecutor, localExecutor } from './local-executor.js'
51
- export { holdGroups, killTree, untilGroupsGone } from './kill-tree.js'
52
- export { sampleTrees } from './proc-sample.js'
54
+ export { holdGroups, killTree, pauseTree, untilGroupsGone } from './kill-tree.js'
55
+ export { sampleTrees, type TreeUsage } from './proc-sample.js'
53
56
  export {
54
57
  assertExecuteResult,
55
58
  executorFallback,
59
+ hostPlatform,
56
60
  isExecutorFallback,
57
61
  selectExecutor,
58
62
  type ExecuteRequest,
@@ -328,6 +328,21 @@ export function killTree(child: Child, signal: 'SIGINT' | 'SIGTERM' | 'SIGKILL')
328
328
  }
329
329
  }
330
330
 
331
+ /**
332
+ * Stop or continue `child`'s group with vx (Ctrl-Z, `fg`). Straight to
333
+ * the group, never down a sandbox's channel: its watcher passes one
334
+ * signal and is gone. Under a Linux sandbox that stops bwrap, and the
335
+ * command inside blocks once its output pipe fills.
336
+ */
337
+ export function pauseTree(child: Child, signal: 'SIGSTOP' | 'SIGCONT'): void {
338
+ if (!(child.pid > 0) || goneGroups.has(child)) return
339
+ try {
340
+ process.kill(-child.pid, signal)
341
+ } catch {
342
+ // The group is gone, or not ours: nothing of it to stop.
343
+ }
344
+ }
345
+
331
346
  /**
332
347
  * How often a group whose leader has exited is asked again whether any
333
348
  * member is left. Only a group that outlived its leader is polled.
@@ -2,7 +2,7 @@
2
2
  // process. Not a plugin — "run it on this machine" is not a capability
3
3
  // someone else supplies differently, it is what running MEANS when no
4
4
  // plugin claims the task, so it sits at the TAIL of every executor list
5
- // (owner, 2026-09-05). An executor plugin places work ELSEWHERE; its
5
+ // (2026-09-05). An executor plugin places work ELSEWHERE; its
6
6
  // absence is "here".
7
7
  import { runCommand, signalExitCode, stopSignal } from './runner.js'
8
8
  import { runSandboxed } from './sandbox-runtime.js'
@@ -8,7 +8,7 @@
8
8
  // counted; nor is the CPU of a descendant that already exited.
9
9
 
10
10
  import { readdir, readFile } from 'node:fs/promises'
11
- import { procfsIsOwn } from '../util/index.js'
11
+ import { helperEnv, procfsIsOwn } from '../util/index.js'
12
12
 
13
13
  export interface TreeUsage {
14
14
  /** CPU time of the live tree, user + system, in ms. */
@@ -125,7 +125,9 @@ async function linuxRssKiB(pid: number): Promise<number> {
125
125
  async function psTable(): Promise<Map<number, Proc> | undefined> {
126
126
  let text: string
127
127
  try {
128
+ // It runs beside the tasks, readable by them (`ps eww`): PATH alone.
128
129
  const ps = Bun.spawn(['ps', '-A', '-o', 'pid=,ppid=,rss=,time='], {
130
+ env: helperEnv(),
129
131
  stdin: 'ignore',
130
132
  stdout: 'pipe',
131
133
  stderr: 'ignore',