@vzn/vx 0.0.644 → 0.0.646

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 (83) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +22 -48
  3. package/package.json +5 -5
  4. package/schemas/init.json +5 -0
  5. package/schemas/last.json +15 -0
  6. package/skills/vx/SKILL.md +1 -1
  7. package/src/bin.ts +1 -1
  8. package/src/cache/archive.ts +12 -76
  9. package/src/cache/cache.ts +72 -42
  10. package/src/cache/git-inputs.ts +55 -10
  11. package/src/cache/index.ts +5 -2
  12. package/src/cache/inputs.ts +61 -11
  13. package/src/cache/layer.ts +30 -8
  14. package/src/cache/layered-cache.ts +138 -44
  15. package/src/cache/output-index.ts +25 -11
  16. package/src/cache/plugin-cache.ts +9 -14
  17. package/src/cache/remote-lane.ts +60 -22
  18. package/src/cache/run-history.ts +13 -6
  19. package/src/cache/schema.ts +19 -2
  20. package/src/cache/scoped-cache.ts +3 -80
  21. package/src/cli/core-alias.ts +5 -2
  22. package/src/cli/help.ts +8 -10
  23. package/src/cli/index.ts +0 -12
  24. package/src/cli/init.ts +159 -25
  25. package/src/cli/last.ts +4 -2
  26. package/src/cli/plan-format.ts +6 -2
  27. package/src/cli/plugin-commands.ts +98 -6
  28. package/src/cli/plugin-templates.ts +2 -2
  29. package/src/cli/run.ts +2 -8
  30. package/src/cli/select.ts +5 -3
  31. package/src/cli/upgrade.ts +5 -6
  32. package/src/config.ts +2 -4
  33. package/src/exec/executor.ts +112 -9
  34. package/src/exec/index.ts +2 -0
  35. package/src/exec/proc-sample.ts +67 -5
  36. package/src/exec/runner.ts +4 -0
  37. package/src/exec/sandbox-runtime.ts +11 -4
  38. package/src/graph/scheduler.ts +59 -8
  39. package/src/history/history.ts +119 -7
  40. package/src/history/metrics.ts +53 -16
  41. package/src/index.ts +16 -1
  42. package/src/orchestrator/affected-tasks.ts +0 -18
  43. package/src/orchestrator/execute-args.ts +3 -0
  44. package/src/orchestrator/execute-task.ts +28 -7
  45. package/src/orchestrator/held-servers.ts +86 -0
  46. package/src/orchestrator/hit-restore.ts +11 -4
  47. package/src/orchestrator/index.ts +18 -1
  48. package/src/orchestrator/local-floor.ts +104 -0
  49. package/src/orchestrator/local-shortcircuit.ts +26 -7
  50. package/src/orchestrator/miss-save.ts +10 -4
  51. package/src/orchestrator/options.ts +0 -8
  52. package/src/orchestrator/persistent-task.ts +14 -0
  53. package/src/orchestrator/placement.ts +84 -9
  54. package/src/orchestrator/plan.ts +7 -1
  55. package/src/orchestrator/plugin-host.ts +22 -7
  56. package/src/orchestrator/plugin.ts +48 -7
  57. package/src/orchestrator/prepare.ts +14 -49
  58. package/src/orchestrator/run-context.ts +39 -1
  59. package/src/orchestrator/run-keys.ts +164 -0
  60. package/src/orchestrator/run-records.ts +6 -0
  61. package/src/orchestrator/run.ts +95 -6
  62. package/src/orchestrator/sandbox-request.ts +9 -3
  63. package/src/orchestrator/task-artifacts.ts +142 -0
  64. package/src/orchestrator/task-hash.ts +12 -4
  65. package/src/orchestrator/task-process.ts +16 -0
  66. package/src/orchestrator/telemetry-host.ts +2 -1
  67. package/src/orchestrator/telemetry.ts +60 -17
  68. package/src/report/forecast.ts +6 -27
  69. package/src/report/framed-output.ts +41 -5
  70. package/src/report/logger.ts +60 -20
  71. package/src/report/status-line.ts +38 -18
  72. package/src/report/summary.ts +75 -9
  73. package/src/settings.ts +8 -7
  74. package/src/util/colors.ts +16 -1
  75. package/src/util/index.ts +1 -0
  76. package/src/util/paths.ts +30 -0
  77. package/src/util/ui.ts +4 -1
  78. package/src/workspace/affected.ts +0 -39
  79. package/src/workspace/config-bytecode.ts +276 -0
  80. package/src/workspace/index.ts +0 -2
  81. package/src/workspace/migration.ts +4 -32
  82. package/src/workspace/project-loader.ts +92 -14
  83. package/src/cli/foreign-flags.ts +0 -727
@@ -24,6 +24,7 @@ import { maskedLine, nearest, type PluginSecrets, SecretBroker, UserError } from
24
24
  import type { ProjectMeta } from '../workspace/index.js'
25
25
  import type { EventBus, Logger, RunStartInfo } from '../report/index.js'
26
26
  import type { CiContext } from './run-context.js'
27
+ import type { CacheFacts, TaskArtifacts } from './task-artifacts.js'
27
28
  import type { TelemetryContext, TelemetrySink } from './telemetry.js'
28
29
 
29
30
  /**
@@ -334,6 +335,20 @@ export interface CommandContext extends BaseContext {
334
335
  * workspace's cache plugins supply, under the run's scope. Opened on first use.
335
336
  */
336
337
  readonly cache: PluginCache
338
+ /**
339
+ * Task entries by key in the workspace's cache, under a run's policy and
340
+ * scope (a laptop writes no remote unless the workspace's policy names
341
+ * it): what a verb that runs a run's tasks on this machine saves and
342
+ * restores through (`TaskArtifacts`). Opened on first use.
343
+ */
344
+ readonly artifacts: TaskArtifacts
345
+ /**
346
+ * `artifacts` read as another machine's run would read them: under its
347
+ * refs, with the access its remotes lent (`CacheFacts`, asked on each
348
+ * request, so a token it lends again replaces the last). Opened on first
349
+ * use; until `facts` answers, under this machine's own.
350
+ */
351
+ artifactsFor(facts: () => CacheFacts | undefined): TaskArtifacts
337
352
  }
338
353
 
339
354
  /** `config` runs before the cache dir is known — it may be what the hook changes. */
@@ -346,10 +361,9 @@ export interface WorkspaceHookContext {
346
361
  * What a CI knows of the run's branch (owner, 2026-10-17): `scope`, the
347
362
  * cache scope (`'vznjs/vx:feat-x'`; `VX_CACHE_SCOPE` beats it, and it
348
363
  * beats the one git names), and `base`, the commit the branch is compared
349
- * with (a pull request's base), for a bare `--affected` and for which
350
- * tasks read the scope. `trusted`: the run has no scope, whatever git
351
- * names, and reads and writes only the default keys (a merge queue's run,
352
- * owner 2026-10-18). A later plugin's field wins.
364
+ * with (a pull request's base), for a bare `--affected`. `trusted`: the
365
+ * run has no scope, whatever git names (a merge queue's run, owner
366
+ * 2026-10-18). The scope keys plugin entries only. A later plugin's field wins.
353
367
  */
354
368
  export interface BranchFacts {
355
369
  scope?: string
@@ -501,16 +515,35 @@ export interface ScheduleHookContext extends BaseContext {
501
515
  * from a remote). Ignored in a plan and once the run's tasks are done.
502
516
  */
503
517
  refine(weights: ReadonlyMap<string, number>): void
518
+ /**
519
+ * Calls `listener` each time another plugin `refine`s: the late data it
520
+ * waited for (timings fetched from a remote) has landed in history, so a
521
+ * plugin that read history in `schedule` may read it again. Never called
522
+ * in a plan.
523
+ */
524
+ onRefine(listener: () => void): void
504
525
  }
505
526
 
506
527
  /** A `cache` hook's context: the workspace and the run's scope, never the local store or the policy, which core owns. */
507
528
  export interface CacheContext extends BaseContext {
508
529
  /**
509
- * The run's cache scope (`'vznjs/vx:feat-x'`), `undefined` when none. Core already keys a scoped run's
510
- * writes apart; a remote reads it to pick a credential or check a token's
511
- * claims (an OIDC branch) for every call it makes.
530
+ * The run's cache scope (`'vznjs/vx:feat-x'`), `undefined` when none: what
531
+ * plugins' `ctx.cache` entries and stored runs are keyed apart by.
512
532
  */
513
533
  readonly scope: string | undefined
534
+ /**
535
+ * The refs the run reads, in order (`['refs/heads/feat-x', 'refs/heads/main']`):
536
+ * its own branch, the branch it merges into, the default branch. A remote
537
+ * keys or scopes its entries by them; core keys task entries by none.
538
+ */
539
+ readonly refs: readonly string[]
540
+ /**
541
+ * The token a run lent this plugin's remote (`RemoteCacheLayer.lend`),
542
+ * on a machine that runs that run's tasks (`CommandContext.artifactsFor`);
543
+ * undefined anywhere else. Read it per request: a run lends again before
544
+ * a token ends. A plugin uses it only when it has no access of its own.
545
+ */
546
+ readonly lent?: () => string | undefined
514
547
  }
515
548
 
516
549
  export interface OutputContext extends BaseContext {
@@ -523,6 +556,14 @@ export interface OutputContext extends BaseContext {
523
556
  export interface ExecutorContext extends BaseContext {
524
557
  /** The run's worker count — an executor that paces itself reads it here. */
525
558
  readonly concurrency: number
559
+ /** Task entries by key in the run's cache, for a task another machine ran (`TaskArtifacts`). */
560
+ readonly artifacts: TaskArtifacts
561
+ /**
562
+ * What a machine that runs this run's tasks reads the cache under
563
+ * (`CacheFacts`): the run's refs, and each remote's lent access, asked
564
+ * now. Ask again before `expiresAt`.
565
+ */
566
+ cacheFacts(): Promise<CacheFacts>
526
567
  }
527
568
 
528
569
  export interface PluginContext extends PluginSetupContext {
@@ -26,7 +26,6 @@ import {
26
26
  writesRunStore,
27
27
  type PluginCache,
28
28
  type ResolvedRemote,
29
- BranchScope,
30
29
  applyGitEnumeration,
31
30
  gitPathspecs,
32
31
  MAX_SCOPED_PATHSPECS,
@@ -34,14 +33,11 @@ import {
34
33
  lazyGitEnumeration,
35
34
  measureFsClockLag,
36
35
  DEFAULT_STORE,
37
- type StoreOptions,
38
36
  } from '../cache/index.js'
39
37
  import {
40
38
  buildPackageGraph,
41
39
  computeNestedProjectDirs,
42
40
  findWorkspaceRoot,
43
- type AffectedChanges,
44
- branchChanges,
45
41
  configuredBaseBranch,
46
42
  type LoadReads,
47
43
  PROJECT_CONFIG_FILENAMES,
@@ -81,13 +77,13 @@ import {
81
77
  lockReader,
82
78
  type LoadedProjects,
83
79
  } from './projects.js'
84
- import { keptByAffected, keyedByChange } from './affected-tasks.js'
80
+ import { keptByAffected } from './affected-tasks.js'
85
81
  import { applyGraphStage } from './graph-stage.js'
86
82
  import { keyExcludedDependencies } from './excluded-keys.js'
87
83
  import { FingerprintWatch } from './fingerprint-watch.js'
88
84
  import type { VxPlugin } from './plugin.js'
89
85
  import { askIgnoredOutputs, gatedOutputs } from './stable-keys.js'
90
- import { currentBranch, resolveCacheScope } from './run-context.js'
86
+ import { cacheRefs, currentBranch, resolveCacheScope } from './run-context.js'
91
87
  import { createHashCache, type HashCache } from './task-hash.js'
92
88
  import { type Logger, warnOf } from '../report/index.js'
93
89
  import type { RunOptions } from './options.js'
@@ -120,6 +116,8 @@ export interface PreparedRun {
120
116
  cacheScope: string | undefined
121
117
  /** The run's remotes in declared order, as the policy gates each; `openRemotes` turns one off. */
122
118
  remotes: ResolvedRemote[]
119
+ /** The refs the run reads, in order (`CacheContext.refs`). */
120
+ cacheRefs: readonly string[]
123
121
  /**
124
122
  * Scheduling priorities from plugins' `schedule` stage (task id → weight,
125
123
  * merged over the structural baseline by the scheduler). Empty when no
@@ -291,33 +289,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
291
289
  reused !== undefined
292
290
  ? Promise.resolve(reused.projects)
293
291
  : discoverProjects(workspaceRoot, readLock, plugins, cacheDir, warnOf(log), git)
294
- // What the branch changed decides which tasks read the scope (owner,
295
- // 2026-10-17). Only a scoped run's first remote call asks git: a run the
296
- // local cache answers makes no remote call, and diffing beside discovery
297
- // cost it 20-60 ms at 1k-5k projects.
298
- const scopeChanges: (() => Promise<AffectedChanges>) | undefined =
299
- cacheScope === undefined
300
- ? undefined
301
- : options.scopeChanges !== undefined
302
- ? async () => options.scopeChanges!
303
- : () =>
304
- branchChanges(
305
- workspaceRoot,
306
- options.scopeBase ?? branchFacts.base,
307
- baseBranch,
308
- discovering,
309
- {
310
- untracked: async () => (await git.start()).untracked,
311
- // A scoped enumeration lists only its projects: null asks git.
312
- links: async () => {
313
- const g = await git.start()
314
- if (!g.scopes.includes('.')) return null
315
- const out: string[] = []
316
- for (const [rel, oid] of g.trusted) if (oid.startsWith('120000:')) out.push(rel)
317
- return out
318
- },
319
- },
320
- )
321
292
  const projectMetas = await discovering
322
293
  mark('discover projects')
323
294
 
@@ -383,8 +354,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
383
354
  DEFAULT_STORE.embeddings) as number,
384
355
  decompress: (resolveSetting('cache.store.decompress', workspaceConfig).value ??
385
356
  DEFAULT_STORE.decompress) as boolean,
386
- verify: (resolveSetting('cache.store.verify', workspaceConfig).value ??
387
- DEFAULT_STORE.verify) as StoreOptions['verify'],
388
357
  },
389
358
  )
390
359
  const unwritable = localCache.readOnlyNotice()
@@ -519,9 +488,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
519
488
  // cache plugin.
520
489
  let cache: CacheLayer
521
490
  const remotes: ResolvedRemote[] = []
522
- // A scope only picks remote keys (owner, 2026-10-17): the local store
523
- // keeps every branch's entries under the task keys.
524
- const branchScope = cacheScope === undefined ? undefined : new BranchScope(cacheScope)
525
491
  if (options.remoteCache) {
526
492
  remotes.push({
527
493
  remote: options.remoteCache,
@@ -530,18 +496,20 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
530
496
  onError: (err) => warnOf(log)(`[vx] remote cache: ${err.message}`),
531
497
  })
532
498
  }
499
+ // An injected remote is no plugin's: nothing reads refs for it.
500
+ const refs = options.remoteCache ? [] : cacheRefs(workspaceRoot, baseBranch)
533
501
  try {
534
502
  cache = options.remoteCache
535
- ? new LayeredCache(localCache, remotes, branchScope, options.uploadConcurrency)
503
+ ? new LayeredCache(localCache, remotes, options.uploadConcurrency)
536
504
  : await resolveCache(plugins, {
537
505
  workspaceRoot,
538
506
  cacheDir,
539
507
  warn: warnOf(log),
540
508
  scope: cacheScope,
509
+ refs,
541
510
  localCache,
542
511
  policy,
543
512
  remotes,
544
- ...(branchScope !== undefined ? { branch: branchScope } : {}),
545
513
  ...(options.uploadConcurrency !== undefined
546
514
  ? { uploadConcurrency: options.uploadConcurrency }
547
515
  : {}),
@@ -590,6 +558,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
590
558
  cachePolicy: policy,
591
559
  cacheScope,
592
560
  remotes,
561
+ cacheRefs: refs,
593
562
  keepRunEntry: undefined,
594
563
  priorities: () => Promise.resolve(new Map()),
595
564
  branch,
@@ -776,13 +745,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
776
745
  if (hasHook(plugins, 'key')) {
777
746
  await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: warnOf(log) })
778
747
  }
779
- // The final inputs decide which tasks the branch changed: they read
780
- // the scope, every other task the default key.
781
- if (branchScope !== undefined && scopeChanges !== undefined) {
782
- branchScope.routeTasks(async () =>
783
- keyedByChange(nodes, await scopeChanges(), projects, packageGraph),
784
- )
785
- }
786
748
  refuseFiltersThroughGroups(nodes)
787
749
  const exclude = options.excludeDependencies
788
750
  let keyOnly: ReadonlyMap<string, TaskNode> = new Map()
@@ -847,6 +809,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
847
809
  cachePolicy: policy,
848
810
  cacheScope,
849
811
  remotes,
812
+ cacheRefs: refs,
850
813
  priorities,
851
814
  branch,
852
815
  cacheFor,
@@ -958,7 +921,7 @@ function pruneToClosure(nodes: Map<string, TaskNode>, roots: ReadonlySet<string>
958
921
  }
959
922
  }
960
923
 
961
- const namesRemote = (spec: string | undefined): boolean =>
924
+ export const namesRemote = (spec: string | undefined): boolean =>
962
925
  spec !== undefined && /remote:[rw]/.test(spec)
963
926
 
964
927
  /**
@@ -966,7 +929,9 @@ const namesRemote = (spec: string | undefined): boolean =>
966
929
  * since the grammar lives in the cache module the workspace module may not
967
930
  * import. Undefined when it names none.
968
931
  */
969
- function workspaceCachePolicy(config: WorkspaceConfig | null | undefined): CachePolicy | undefined {
932
+ export function workspaceCachePolicy(
933
+ config: WorkspaceConfig | null | undefined,
934
+ ): CachePolicy | undefined {
970
935
  const spec = config?.cache?.policy
971
936
  if (spec === undefined) return undefined
972
937
  try {
@@ -553,6 +553,44 @@ export function localBranchScope(
553
553
  return branchCacheScope(repo, name)
554
554
  }
555
555
 
556
+ /**
557
+ * The refs a remote reads, in order (2026-10-20): the run's own branch,
558
+ * then the branch it merges into, then the default branch, each once. On
559
+ * CI from its env (a GitHub pull request's head branch, a merge queue's
560
+ * queue ref, a GitLab pipeline's ref), else from git: the checked-out
561
+ * branch, then the default (`origin/HEAD`). A remote plugin keys or scopes
562
+ * its entries by them (`scope` on turboCache/nxCache); core keys nothing
563
+ * by them.
564
+ */
565
+ export function cacheRefs(
566
+ workspaceRoot: string,
567
+ baseBranch: string | undefined,
568
+ env: NodeJS.ProcessEnv = process.env,
569
+ ): string[] {
570
+ const set = (v: string | undefined): string | undefined =>
571
+ v !== undefined && v.trim() !== '' ? v.trim() : undefined
572
+ const head = (b: string | undefined) => (b === undefined ? undefined : `refs/heads/${b}`)
573
+ let own: string | undefined
574
+ let base: string | undefined
575
+ if (env['GITHUB_ACTIONS'] === 'true') {
576
+ own = head(set(env['GITHUB_HEAD_REF'])) ?? set(env['GITHUB_REF'])
577
+ base = head(set(env['GITHUB_BASE_REF']))
578
+ const queue = /^refs\/heads\/gh-readonly-queue\/(.+)\/pr-\d+-[0-9a-f]+$/.exec(own ?? '')
579
+ if (queue !== null) base = head(queue[1])
580
+ } else if (env['GITLAB_CI'] === 'true') {
581
+ const tag = set(env['CI_COMMIT_TAG'])
582
+ own = tag !== undefined ? `refs/tags/${tag}` : head(set(env['CI_COMMIT_REF_NAME']))
583
+ base = head(set(env['CI_MERGE_REQUEST_TARGET_BRANCH_NAME']))
584
+ } else {
585
+ own = head(
586
+ (readHeadDirect(workspaceRoot) ?? captureGitContext(workspaceRoot, null, env)).branch ??
587
+ undefined,
588
+ )
589
+ }
590
+ const trunk = head(baseBranch ?? captureDefaultBranch(env, workspaceRoot) ?? undefined)
591
+ return [...new Set([own, base, trunk].filter((r): r is string => r !== undefined))]
592
+ }
593
+
556
594
  /**
557
595
  * The repository's path on its host (`owner/repo`; a GitLab subgroup's
558
596
  * `group/sub/repo` whole, as `gitlab()` names it); undefined for a local path.
@@ -591,7 +629,7 @@ function gitConfigSections(workspaceRoot: string): Map<string, Map<string, strin
591
629
  * The run's cache scope (owner, 2026-10-17): `VX_CACHE_SCOPE` (empty: none),
592
630
  * else a plugin's (`setBranch`, `github()` on Actions; null: the plugin
593
631
  * named the run trusted), else, with a cache plugin to share it, the
594
- * branch's from git. Undefined: the default keys.
632
+ * branch's from git. Undefined: no scope. It keys plugin entries only.
595
633
  */
596
634
  export function resolveCacheScope(
597
635
  workspaceRoot: string,
@@ -0,0 +1,164 @@
1
+ // The run key memo: every up-front key a warm run derived, read back whole
2
+ // when nothing any of them folds has moved. Deriving them again cost 250 ms
3
+ // of a 1,070 ms warm run at 9,600 tasks even with every fold memo row
4
+ // answering (2026-10-22): the per-task walk itself, not the files.
5
+ //
6
+ // The token is everything `deriveStableKeys` reads: each node's id, edges,
7
+ // resolved config, plugin key parts and files token, the declared env
8
+ // values, the forward args and the code that derives. A run the token cannot
9
+ // vouch for (a keyless or runtime-probed task, a declaration no files token
10
+ // covers) derives as before.
11
+
12
+ import { readdirSync, statSync } from 'node:fs'
13
+ import path from 'node:path'
14
+ import type { GitFilesCache } from '../cache/index.js'
15
+ import type { TaskNode } from '../graph/index.js'
16
+ import { xxh3hex } from '../util/index.js'
17
+ import type { StableKey } from './stable-keys.js'
18
+ import { hashTaskConfig, isKeyless, taskFilesToken, type HashCache } from './task-hash.js'
19
+
20
+ /** Moves every remembered run: bump it when what a token stands for changes. */
21
+ const RUN_MEMO_VERSION = 'run-memo-v1'
22
+
23
+ export interface RunKeysArgs {
24
+ nodes: Map<string, TaskNode>
25
+ workspaceRoot: string
26
+ claimedFiles: readonly string[]
27
+ forwardArgs?: readonly string[] | undefined
28
+ nestedDirsByProject: Map<string, string[]>
29
+ gitFilesCache: GitFilesCache
30
+ hashCache: HashCache
31
+ }
32
+
33
+ /** What a run's up-front pass yields, as the memo holds it. */
34
+ export interface RunKeys {
35
+ stableKeys: StableKey[]
36
+ uncachedKeys: Map<string, string>
37
+ }
38
+
39
+ /**
40
+ * The memo row's name (the selection) and the token its keys stand for, or
41
+ * undefined when a node's key reads something the token cannot name.
42
+ */
43
+ export function runKeysToken(args: RunKeysArgs): { id: string; token: string } | undefined {
44
+ if (args.hashCache.folds === undefined) return undefined
45
+ const parts: string[] = [
46
+ RUN_MEMO_VERSION,
47
+ codeIdentity(),
48
+ args.workspaceRoot,
49
+ JSON.stringify(args.forwardArgs ?? []),
50
+ JSON.stringify([...args.claimedFiles].sort()),
51
+ JSON.stringify([...args.nestedDirsByProject]),
52
+ // Which outputs git ignores decides which a reader may see (`readableOutputs`).
53
+ [...args.gitFilesCache.ignoredOutputs].sort().join('\0'),
54
+ ]
55
+ const ids: string[] = []
56
+ const envNames = new Set<string>()
57
+ for (const node of args.nodes.values()) {
58
+ const cfg = node.config
59
+ if (isKeyless(cfg) || node.excludedUpstream !== undefined) return undefined
60
+ const inputs = cfg.inputs
61
+ if ((inputs?.runtime?.length ?? 0) > 0 || (inputs?.workspaceRuntime?.length ?? 0) > 0) {
62
+ return undefined
63
+ }
64
+ let files = ''
65
+ if (inputs !== undefined) {
66
+ if ((inputs.workspaceFiles?.length ?? 0) > 0) return undefined
67
+ // No positive entry resolves no files (`filesPlan`).
68
+ if (inputs.files === undefined || inputs.files.some((e) => !e.startsWith('!'))) {
69
+ const token = taskFilesToken({
70
+ node,
71
+ workspaceRoot: args.workspaceRoot,
72
+ nestedProjectDirs: args.nestedDirsByProject.get(node.projectName) ?? [],
73
+ gitFilesCache: args.gitFilesCache,
74
+ hashCache: args.hashCache,
75
+ })
76
+ if (token === undefined) return undefined
77
+ files = token
78
+ }
79
+ for (const name of inputs.env ?? []) envNames.add(name)
80
+ }
81
+ ids.push(node.id)
82
+ parts.push(
83
+ node.id,
84
+ node.projectName,
85
+ node.projectDir,
86
+ node.deps.join('\u0001'),
87
+ (node.orderOnly ?? []).join('\u0001'),
88
+ node.requested ? '1' : '0',
89
+ hashTaskConfig(cfg, args.hashCache),
90
+ JSON.stringify(node.keyParts ?? []),
91
+ JSON.stringify(node.addsToOutputsOf ?? []),
92
+ JSON.stringify(node.outputsAddedToBy ?? []),
93
+ files,
94
+ )
95
+ }
96
+ for (const name of [...envNames].sort()) {
97
+ const value = process.env[name]
98
+ parts.push(value === undefined ? name : `${name}=${value}`)
99
+ }
100
+ return { id: `\0run\0${xxh3hex(ids.join('\0'))}`, token: xxh3hex(parts.join('\0')) }
101
+ }
102
+
103
+ /** The keys remembered under `token`, mapped back onto this run's nodes. */
104
+ export function readRunKeys(
105
+ args: RunKeysArgs,
106
+ at: { id: string; token: string },
107
+ ): RunKeys | undefined {
108
+ const row = args.hashCache.folds?.get(at.id, at.token)
109
+ if (row === undefined) return undefined
110
+ let parsed: { s: [string, string][]; u: [string, string][] }
111
+ try {
112
+ parsed = JSON.parse(row)
113
+ } catch {
114
+ return undefined
115
+ }
116
+ const stableKeys: StableKey[] = []
117
+ for (const [id, hash] of parsed.s) {
118
+ const node = args.nodes.get(id)
119
+ if (node === undefined) return undefined
120
+ stableKeys.push({ hash, node })
121
+ }
122
+ return { stableKeys, uncachedKeys: new Map(parsed.u) }
123
+ }
124
+
125
+ export function writeRunKeys(
126
+ args: RunKeysArgs,
127
+ at: { id: string; token: string },
128
+ keys: RunKeys,
129
+ ): void {
130
+ args.hashCache.folds?.set(
131
+ at.id,
132
+ at.token,
133
+ JSON.stringify({
134
+ s: keys.stableKeys.map((k) => [k.node.id, k.hash]),
135
+ u: [...keys.uncachedKeys],
136
+ }),
137
+ )
138
+ }
139
+
140
+ let identity: string | undefined
141
+
142
+ /**
143
+ * The code that derives the keys: a compiled binary is its file; a source
144
+ * checkout is every module under `src/`, so an edit to the derivation from
145
+ * source never reads back a key the old code made.
146
+ */
147
+ function codeIdentity(): string {
148
+ if (identity !== undefined) return identity
149
+ // `Bun.main`, not `import.meta.dir`: `--minify` inlines the build
150
+ // machine's source path, so an installed binary walked a `src/` that is
151
+ // not there, threw, and lost every warm run's short circuit (2026-10-22).
152
+ if (Bun.main.startsWith('/$bunfs/')) {
153
+ const st = statSync(process.execPath)
154
+ return (identity = `${process.execPath}:${st.size}:${st.mtimeMs}:${st.ino}`)
155
+ }
156
+ const src = path.resolve(import.meta.dir, '..')
157
+ const stamps: string[] = []
158
+ for (const rel of readdirSync(src, { recursive: true, encoding: 'utf8' }).sort()) {
159
+ if (!rel.endsWith('.ts')) continue
160
+ const st = statSync(path.join(src, rel))
161
+ stamps.push(`${rel}:${st.size}:${st.mtimeMs}`)
162
+ }
163
+ return (identity = xxh3hex(stamps.join('\0')))
164
+ }
@@ -113,6 +113,11 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
113
113
  runId,
114
114
  ...(o.cpuMs !== undefined ? { cpuMs: o.cpuMs } : {}),
115
115
  ...(o.peakRssBytes !== undefined ? { peakRssBytes: o.peakRssBytes } : {}),
116
+ ...(o.peakCores !== undefined ? { peakCores: o.peakCores } : {}),
117
+ ...(o.avgRssBytes !== undefined ? { avgRssBytes: o.avgRssBytes } : {}),
118
+ ...(o.where !== undefined ? { placement: o.where } : {}),
119
+ ...(o.whereCores !== undefined ? { placementCores: o.whereCores } : {}),
120
+ ...(o.whereMemoryBytes !== undefined ? { placementMemoryBytes: o.whereMemoryBytes } : {}),
116
121
  ...(o.wallclockStartNs !== undefined ? { wallclockStartNs: o.wallclockStartNs } : {}),
117
122
  ...(o.wallclockEndNs !== undefined ? { wallclockEndNs: o.wallclockEndNs } : {}),
118
123
  cacheHit: isCacheHit(o.status),
@@ -126,6 +131,7 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
126
131
  ...(o.sandboxViolations !== undefined ? { sandboxViolations: o.sandboxViolations } : {}),
127
132
  ...(o.notReady !== undefined ? { notReady: o.notReady } : {}),
128
133
  ...(remoteError !== undefined ? { remoteError } : {}),
134
+ ...(o.keylessBy !== undefined ? { keylessBy: o.keylessBy } : {}),
129
135
  })
130
136
  if (o.status === 'failed') failedCount++
131
137
  if (o.status === 'cache-hit') hitLocalCount++