nx 23.3.0 → 24.0.0-beta.1

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 (114) hide show
  1. package/README.md +3 -6
  2. package/dist/nx-cloud-internals.d.ts +1 -0
  3. package/dist/nx-cloud-internals.js +7 -1
  4. package/dist/src/adapter/ngcli-adapter.js +1 -0
  5. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +0 -12
  6. package/dist/src/command-line/graph/graph.js +3 -5
  7. package/dist/src/command-line/init/command-object.js +0 -12
  8. package/dist/src/command-line/init/init-v2.js +0 -2
  9. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +1 -1
  10. package/dist/src/command-line/migrate/agentic/close-agent-session.js +4 -3
  11. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +76 -15
  12. package/dist/src/command-line/migrate/execute-migration.d.ts +2 -2
  13. package/dist/src/command-line/migrate/execute-migration.js +18 -10
  14. package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -1
  15. package/dist/src/command-line/migrate/migrate-commits.js +10 -3
  16. package/dist/src/command-line/migrate/migrate.js +43 -6
  17. package/dist/src/command-line/migrate/run/agent-output.d.ts +1 -0
  18. package/dist/src/command-line/migrate/run/agent-output.js +5 -0
  19. package/dist/src/command-line/migrate/run/broker.d.ts +14 -5
  20. package/dist/src/command-line/migrate/run/broker.js +68 -20
  21. package/dist/src/command-line/migrate/run/index.d.ts +4 -3
  22. package/dist/src/command-line/migrate/run/index.js +8 -1
  23. package/dist/src/command-line/migrate/run/orchestrator.js +72 -13
  24. package/dist/src/command-line/migrate/run/runbook.js +19 -0
  25. package/dist/src/command-line/migrate/run/state-machine.js +4 -1
  26. package/dist/src/command-line/migrate/run/util.d.ts +3 -0
  27. package/dist/src/command-line/migrate/run/util.js +7 -1
  28. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +17 -12
  29. package/dist/src/command-line/release/utils/remote-release-clients/github.js +5 -6
  30. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +5 -6
  31. package/dist/src/command-line/run/executor-utils.js +1 -0
  32. package/dist/src/command-line/run/run-one.d.ts +20 -1
  33. package/dist/src/command-line/run/run-one.js +153 -5
  34. package/dist/src/command-line/show/show-target/info.js +1 -1
  35. package/dist/src/command-line/show/show-target/utils.js +1 -0
  36. package/dist/src/command-line/watch/command-object.js +3 -22
  37. package/dist/src/command-line/watch/watch.d.ts +0 -7
  38. package/dist/src/command-line/watch/watch.js +1 -1
  39. package/dist/src/config/misc-interfaces.d.ts +12 -0
  40. package/dist/src/config/task-graph.d.ts +1 -1
  41. package/dist/src/config/workspace-json-project-json.d.ts +65 -0
  42. package/dist/src/core/graph/main.js +1 -1
  43. package/dist/src/daemon/client/client.d.ts +9 -0
  44. package/dist/src/daemon/client/client.js +89 -37
  45. package/dist/src/daemon/server/shutdown-utils.js +0 -3
  46. package/dist/src/daemon/socket-utils.d.ts +9 -4
  47. package/dist/src/daemon/socket-utils.js +12 -49
  48. package/dist/src/daemon/tmp-dir.d.ts +19 -14
  49. package/dist/src/daemon/tmp-dir.js +142 -354
  50. package/dist/src/devkit-exports.d.ts +1 -1
  51. package/dist/src/devkit-internals.d.ts +1 -1
  52. package/dist/src/devkit-internals.js +1 -0
  53. package/dist/src/hasher/check-task-files.js +4 -4
  54. package/dist/src/hasher/file-hasher.js +9 -7
  55. package/dist/src/hasher/hash-task.js +4 -0
  56. package/dist/src/hasher/native-task-hasher-impl.js +3 -1
  57. package/dist/src/migrations/update-24-0-0/find-custom-hashers.d.ts +3 -0
  58. package/dist/src/migrations/update-24-0-0/find-custom-hashers.js +100 -0
  59. package/dist/src/migrations/update-24-0-0/find-custom-hashers.md +63 -0
  60. package/dist/src/migrations/update-24-0-0/move-custom-hashers-to-inputs.md +145 -0
  61. package/dist/src/native/index.d.ts +180 -12
  62. package/dist/src/native/native-bindings.js +8 -0
  63. package/dist/src/native/nx.wasi-browser.js +1 -0
  64. package/dist/src/native/nx.wasi.cjs +1 -0
  65. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  66. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  67. package/dist/src/nx-cloud/update-manager.d.ts +7 -0
  68. package/dist/src/nx-cloud/update-manager.js +237 -50
  69. package/dist/src/project-graph/affected/affected-tasks.js +1 -0
  70. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +7 -0
  71. package/dist/src/tasks-runner/create-task-graph.d.ts +8 -7
  72. package/dist/src/tasks-runner/create-task-graph.js +47 -26
  73. package/dist/src/tasks-runner/init-tasks-runner.js +13 -0
  74. package/dist/src/tasks-runner/life-cycle.d.ts +4 -1
  75. package/dist/src/tasks-runner/life-cycle.js +7 -0
  76. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +5 -2
  77. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +13 -7
  78. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +4 -3
  79. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +9 -2
  80. package/dist/src/tasks-runner/readiness/probes.d.ts +9 -0
  81. package/dist/src/tasks-runner/readiness/probes.js +62 -0
  82. package/dist/src/tasks-runner/readiness/ready-when.d.ts +38 -0
  83. package/dist/src/tasks-runner/readiness/ready-when.js +190 -0
  84. package/dist/src/tasks-runner/run-command.js +5 -4
  85. package/dist/src/tasks-runner/running-tasks/node-child-process.js +5 -4
  86. package/dist/src/tasks-runner/task-env.d.ts +10 -0
  87. package/dist/src/tasks-runner/task-env.js +26 -5
  88. package/dist/src/tasks-runner/task-graph-utils.d.ts +9 -6
  89. package/dist/src/tasks-runner/task-graph-utils.js +17 -12
  90. package/dist/src/tasks-runner/task-orchestrator.d.ts +37 -8
  91. package/dist/src/tasks-runner/task-orchestrator.js +460 -76
  92. package/dist/src/tasks-runner/tasks-schedule.d.ts +18 -1
  93. package/dist/src/tasks-runner/tasks-schedule.js +64 -6
  94. package/dist/src/tasks-runner/utils.d.ts +20 -5
  95. package/dist/src/tasks-runner/utils.js +70 -35
  96. package/dist/src/ultracache/overrides.js +4 -0
  97. package/dist/src/utils/child-process.js +1 -0
  98. package/dist/src/utils/git-utils.d.ts +6 -1
  99. package/dist/src/utils/git-utils.js +26 -5
  100. package/dist/src/utils/owned-private-dir.d.ts +1 -17
  101. package/dist/src/utils/owned-private-dir.js +10 -62
  102. package/dist/src/utils/perf-hooks.d.ts +1 -0
  103. package/dist/src/utils/plugins/core-plugins.js +1 -1
  104. package/dist/src/utils/spawn-without-terminal.d.ts +27 -0
  105. package/dist/src/utils/spawn-without-terminal.js +110 -0
  106. package/dist/src/utils/string-similarity.d.ts +38 -0
  107. package/dist/src/utils/string-similarity.js +108 -0
  108. package/migrations.json +7 -0
  109. package/package.json +11 -20
  110. package/readme-template.md +3 -3
  111. package/schemas/nx-schema.json +70 -0
  112. package/schemas/project-schema.json +67 -0
  113. package/dist/src/tasks-runner/legacy-depends-on-warning.d.ts +0 -18
  114. package/dist/src/tasks-runner/legacy-depends-on-warning.js +0 -109
@@ -22,6 +22,8 @@ const cache_1 = require("./cache");
22
22
  const forked_process_task_runner_1 = require("./forked-process-task-runner");
23
23
  const is_tui_enabled_1 = require("./is-tui-enabled");
24
24
  const pseudo_terminal_1 = require("./pseudo-terminal");
25
+ const probes_1 = require("./readiness/probes");
26
+ const ready_when_1 = require("./readiness/ready-when");
25
27
  const noop_child_process_1 = require("./running-tasks/noop-child-process");
26
28
  const output_prefix_1 = require("./running-tasks/output-prefix");
27
29
  const shared_running_task_1 = require("./running-tasks/shared-running-task");
@@ -45,6 +47,31 @@ function hasContent(path) {
45
47
  return false;
46
48
  }
47
49
  }
50
+ function createReadinessState(onSettled) {
51
+ const state = {
52
+ settled: false,
53
+ abort: new AbortController(),
54
+ };
55
+ state.promise = new Promise((resolve, reject) => {
56
+ state.resolve = () => {
57
+ if (state.settled)
58
+ return;
59
+ state.settled = true;
60
+ resolve();
61
+ onSettled('ready');
62
+ };
63
+ state.reject = (error) => {
64
+ if (state.settled)
65
+ return;
66
+ state.settled = true;
67
+ reject(error);
68
+ onSettled('failed');
69
+ };
70
+ });
71
+ // A producer with no waiter still settles; that must not be unhandled
72
+ state.promise.catch(() => { });
73
+ return state;
74
+ }
48
75
  class TaskOrchestrator {
49
76
  // endregion internal state
50
77
  constructor(hasher, initiatingProject, initiatingTasks, projectGraph, taskGraph, nxJson, options, bail, daemon,
@@ -78,13 +105,17 @@ class TaskOrchestrator {
78
105
  ? new native_1.RunningTasksService((0, db_connection_1.getLocalDbConnection)())
79
106
  : null;
80
107
  this.taskInvocationTracker = !native_1.IS_WASM
81
- ? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(), Number(process.env.NX_INVOCATION_ROOT_PID ?? process.pid))
108
+ ? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(), (0, task_env_1.getInvocationRootPid)(), (0, task_env_1.getInvocationAncestorPids)())
82
109
  : null;
83
- // Tracks tasks registered by THIS process so that recursive code paths
84
- // (e.g. applyFromCacheOrRunBatch looping on incomplete batches) don't
85
- // re-register and trip the DB uniqueness constraint.
110
+ // Task ids this process has already registered. Recursive code paths (e.g.
111
+ // applyFromCacheOrRunBatch looping on incomplete batches) re-enter the
112
+ // detector for the same task; skipping them saves a DB round trip. A task id
113
+ // is unique within one process, so it is enough to identify the row.
86
114
  this.registeredInvocations = new Set();
87
- this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options);
115
+ this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options, this.fullTaskGraph, {
116
+ readinessElsewhere: (producerId) => this.runningTasksService?.getTaskReadiness(producerId) ?? null,
117
+ onReadinessHold: (producerId) => this.onReadinessHold(producerId),
118
+ });
88
119
  // region internal state
89
120
  this.batchEnv = (0, task_env_1.getEnvVariablesForBatchProcess)(this.options.skipNxCache, this.options.captureStderr);
90
121
  this.reverseTaskDeps = (0, utils_1.calculateReverseDeps)(this.taskGraph);
@@ -123,6 +154,14 @@ class TaskOrchestrator {
123
154
  this.runningDiscreteTasks = new Map();
124
155
  this.discreteTaskExitHandled = new Map();
125
156
  this.continuousTaskExitHandled = new Map();
157
+ // Keyed by producer id: a producer this process started or shares
158
+ this.readiness = new Map();
159
+ // Producers that failed their check while still running: the exit must not
160
+ // report them again, and the saved log gets the reason after the transcript
161
+ this.readinessFailures = new Map();
162
+ this.readinessFailureReasons = new Map();
163
+ this.readinessSettled = false;
164
+ this.waitingLogged = new Set();
126
165
  this.cleanupPromise = null;
127
166
  this.signalHandlers = [];
128
167
  // Tasks whose runner already owns the write to
@@ -212,6 +251,7 @@ class TaskOrchestrator {
212
251
  while (true) {
213
252
  if (this.bailed || this.stopRequested)
214
253
  break;
254
+ await this.scheduleAfterReadiness();
215
255
  // 1. Hash BEFORE processAll so processTask sees hashes set, and so
216
256
  // resolveCachedTasksBulk can look them up in the cache. Each task
217
257
  // is hashed with its own task-specific env (project/target .env
@@ -223,7 +263,7 @@ class TaskOrchestrator {
223
263
  const unhashed = scheduledTasks
224
264
  .map((id) => this.taskGraph.tasks[id])
225
265
  .filter((t) => !t.hash &&
226
- this.taskGraph.dependencies[t.id].every((depId) => this.completedTasks.has(depId)));
266
+ this.taskGraph.dependencies[t.id].every(({ id }) => this.completedTasks.has(id)));
227
267
  if (unhashed.length > 0) {
228
268
  const perTaskEnvs = {};
229
269
  for (const task of unhashed) {
@@ -238,6 +278,11 @@ class TaskOrchestrator {
238
278
  const resolved = await this.resolveCachedTasksBulk();
239
279
  if (resolved)
240
280
  continue;
281
+ // A pass from a continuous loop during the waits above may have
282
+ // admitted tasks this iteration never looked up; they must not reach
283
+ // step 5 without a confirmed miss
284
+ if (this.hasUnresolvedCacheableTask())
285
+ continue;
241
286
  }
242
287
  // 3. Process remaining scheduled tasks (cache misses + non-cacheable).
243
288
  this.processAllScheduledTasks();
@@ -263,6 +308,10 @@ class TaskOrchestrator {
263
308
  break;
264
309
  }
265
310
  // 7. Wait for a worker to finish (woken by scheduleNextTasksAndReleaseThreads)
311
+ // A verdict that landed during this iteration found nothing parked,
312
+ // so run its pass instead of parking
313
+ if (this.readinessSettled)
314
+ continue;
266
315
  await new Promise((res) => this.waitingForTasks.push(res));
267
316
  }
268
317
  }
@@ -272,6 +321,7 @@ class TaskOrchestrator {
272
321
  if (!this.tasksSchedule.hasTasks() || this.bailed || this.stopRequested) {
273
322
  return null;
274
323
  }
324
+ await this.scheduleAfterReadiness();
275
325
  this.processAllScheduledTasks();
276
326
  const task = this.tasksSchedule.nextTask((t) => t.continuous);
277
327
  if (task) {
@@ -293,9 +343,29 @@ class TaskOrchestrator {
293
343
  return null;
294
344
  }
295
345
  // block until some other task completes, then try again
346
+ if (this.readinessSettled)
347
+ continue;
296
348
  await new Promise((res) => this.waitingForTasks.push(res));
297
349
  }
298
350
  }
351
+ hasUnresolvedCacheableTask() {
352
+ return this.tasksSchedule
353
+ .getAllScheduledTasks()
354
+ .scheduledTasks.some((id) => {
355
+ const task = this.taskGraph.tasks[id];
356
+ return (!task.continuous &&
357
+ task.cache &&
358
+ (!task.hash || !this.cacheMissedHashes.has(task.hash)));
359
+ });
360
+ }
361
+ // A verdict releases tasks the schedule has not admitted yet. Whichever
362
+ // loop wakes first runs the pass; the coordinator may be inside a batch.
363
+ async scheduleAfterReadiness() {
364
+ if (this.readinessSettled) {
365
+ this.readinessSettled = false;
366
+ await this.scheduleNextTasksAndReleaseThreads();
367
+ }
368
+ }
299
369
  // region Processing Scheduled Tasks
300
370
  async processTask(taskId) {
301
371
  const task = this.taskGraph.tasks[taskId];
@@ -316,39 +386,59 @@ class TaskOrchestrator {
316
386
  }
317
387
  }
318
388
  /**
319
- * Registers a task invocation and checks for loops across nested Nx processes.
320
- * Uses the task_invocations DB table keyed by root PID. registerTask() throws
321
- * on unique constraint violation when a parent Nx process already registered
322
- * this task — indicating an infinite loop.
389
+ * Registers a task invocation and checks for loops across nested Nx
390
+ * processes. registerTask() returns the invocation chain only when an
391
+ * *ancestor* Nx process is already running this task — a genuine loop.
392
+ * Sibling processes running the same task are legitimate and register
393
+ * without complaint.
394
+ *
395
+ * The overrides hash rides along with the task id so the tracker can tell an
396
+ * ancestor re-invoking a task with different arguments from one repeating
397
+ * the same invocation.
323
398
  */
324
399
  detectTaskInvocationLoop(task) {
325
400
  if (!this.taskInvocationTracker)
326
401
  return;
327
402
  if (this.registeredInvocations.has(task.id))
328
403
  return;
404
+ let chain;
329
405
  try {
330
- this.taskInvocationTracker.registerTask(process.pid, task.id);
406
+ chain = this.taskInvocationTracker.registerTask(process.pid, task.id, (0, utils_1.hashTaskOverrides)(task));
407
+ }
408
+ catch {
409
+ // Loop detection is diagnostic only; a DB failure must not fail the run
410
+ // or be mistaken for a loop.
411
+ return;
412
+ }
413
+ if (!chain) {
331
414
  this.registeredInvocations.add(task.id);
415
+ return;
416
+ }
417
+ const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
418
+ output_1.output.error({
419
+ title: 'Recursive task invocation detected',
420
+ bodyLines: [
421
+ `Nx detected a recursive loop of task invocations:`,
422
+ ``,
423
+ ` ${chainDisplay} -> ${task.id}`,
424
+ ``,
425
+ `Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
426
+ `This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
427
+ `triggers a chain of tasks that eventually re-invokes itself.`,
428
+ ``,
429
+ `To fix this, review the command configuration for the tasks in the chain above.`,
430
+ ],
431
+ });
432
+ process.exit(1);
433
+ }
434
+ releaseTaskInvocation(task) {
435
+ if (!this.registeredInvocations.delete(task.id))
436
+ return;
437
+ try {
438
+ this.taskInvocationTracker?.unregisterTask(process.pid, task.id);
332
439
  }
333
440
  catch {
334
- // Unique constraint violation — task already invoked by an ancestor Nx process
335
- const chain = this.taskInvocationTracker.getInvocationChain();
336
- const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
337
- output_1.output.error({
338
- title: 'Recursive task invocation detected',
339
- bodyLines: [
340
- `Nx detected a recursive loop of task invocations:`,
341
- ``,
342
- ` ${chainDisplay} -> ${task.id}`,
343
- ``,
344
- `Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
345
- `This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
346
- `triggers a chain of tasks that eventually re-invokes itself.`,
347
- ``,
348
- `To fix this, review the command configuration for the tasks in the chain above.`,
349
- ],
350
- });
351
- process.exit(1);
441
+ // Diagnostic only, like registration. A leftover row is swept as stale.
352
442
  }
353
443
  }
354
444
  // endregion Processing Scheduled Tasks
@@ -503,8 +593,7 @@ class TaskOrchestrator {
503
593
  await this.hashBatchTasks(rootTasks);
504
594
  const eligible = [];
505
595
  for (const task of rootTasks) {
506
- const depIds = batch.taskGraph.dependencies[task.id];
507
- const hasNonCachedDep = depIds.some((id) => nonCachedTaskIds.has(id));
596
+ const hasNonCachedDep = batch.taskGraph.dependencies[task.id].some(({ id }) => nonCachedTaskIds.has(id));
508
597
  if (hasNonCachedDep && this.deferredTaskIds?.has(task.id)) {
509
598
  nonCachedTaskIds.add(task.id);
510
599
  needsRehashAfterExecution.add(task.id);
@@ -553,17 +642,19 @@ class TaskOrchestrator {
553
642
  taskIds: Object.keys(batch.taskGraph.tasks),
554
643
  });
555
644
  const { cachedResults, needsRehashAfterExecution } = await this.applyBatchCachedResults(batch, doNotSkipCache, groupId);
556
- // Schedule and start non-cached tasks (cached tasks were already
557
- // started and completed inside applyBatchCachedResults)
558
- const cachedTaskIds = new Set(cachedResults.map((r) => r.task.id));
559
- const nonCachedTasks = tasks.filter((t) => !cachedTaskIds.has(t.id));
645
+ // Cached tasks were started and completed inside applyBatchCachedResults;
646
+ // a member skipped meanwhile (a dependency failed) must not run either
647
+ const nonCachedTasks = tasks.filter((t) => !this.completedTasks.has(t.id));
560
648
  if (nonCachedTasks.length > 0) {
561
649
  await Promise.all(nonCachedTasks.map((task) => this.options.lifeCycle.scheduleTask(task)));
562
650
  await this.preRunSteps(nonCachedTasks, { groupId });
563
651
  }
564
652
  // Phase 2: Run non-cached tasks, then re-hash tasks that read their outputs
565
- const taskIdsToSkip = cachedResults.map((r) => r.task.id);
566
653
  let batchResults = [];
654
+ // Read again: a member can be skipped while the hooks above ran
655
+ const taskIdsToSkip = tasks
656
+ .filter((t) => this.completedTasks.has(t.id))
657
+ .map((t) => t.id);
567
658
  if (taskIdsToSkip.length < tasks.length) {
568
659
  const runGraph = (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, taskIdsToSkip);
569
660
  for (const task of Object.values(runGraph.tasks)) {
@@ -923,8 +1014,7 @@ class TaskOrchestrator {
923
1014
  // Wake coordinator — the delete above may satisfy the exit condition
924
1015
  // (pendingDiscreteWorkers.size === 0) that was missed when
925
1016
  // scheduleNextTasksAndReleaseThreads fired earlier.
926
- this.waitingForTasks.forEach((f) => f(null));
927
- this.waitingForTasks.length = 0;
1017
+ this.releaseThreads();
928
1018
  });
929
1019
  this.pendingDiscreteWorkers.add(worker);
930
1020
  }
@@ -947,13 +1037,36 @@ class TaskOrchestrator {
947
1037
  await this.postRunSteps([{ task, status: 'failure', terminalOutput }], doNotSkipCache, groupId);
948
1038
  }
949
1039
  /**
950
- * Spawn and wait on a task's child process, unconditionally — no cache
951
- * lookup. Callers must have already confirmed the task is a cache miss
952
- * (or disabled caching entirely).
1040
+ * Spawn and wait on a task's child process once its ready dependencies are
1041
+ * ready, with no cache lookup. Callers must have already confirmed the task
1042
+ * is a cache miss (or disabled caching entirely).
953
1043
  */
954
1044
  async runTaskDirectly(doNotSkipCache, task, groupId) {
955
1045
  // Wait for task to be processed
956
1046
  const taskSpecificEnv = await this.processedTasks.get(task.id);
1047
+ const waitError = await this.waitForReadyDependencies(task).then(() => null, (e) => e);
1048
+ // Skipped by a failed dependency's propagation while waiting
1049
+ if (this.completedTasks.has(task.id)) {
1050
+ return {
1051
+ task,
1052
+ code: 1,
1053
+ status: this.completedTasks.get(task.id),
1054
+ terminalOutput: '',
1055
+ };
1056
+ }
1057
+ if (waitError instanceof ready_when_1.ProducerFailedError) {
1058
+ await this.skipWaiter(task, groupId);
1059
+ return { task, code: 1, status: 'skipped', terminalOutput: '' };
1060
+ }
1061
+ if (waitError) {
1062
+ await this.failTaskBeforeStart(task, groupId, waitError, doNotSkipCache);
1063
+ return {
1064
+ task,
1065
+ code: 1,
1066
+ status: 'failure',
1067
+ terminalOutput: waitError.message,
1068
+ };
1069
+ }
957
1070
  await this.preRunSteps([task], { groupId });
958
1071
  const pipeOutput = await this.pipeOutputCapture(task);
959
1072
  const temporaryOutputPath = this.cache.temporaryOutputPath(task);
@@ -1165,11 +1278,28 @@ class TaskOrchestrator {
1165
1278
  async startContinuousTask(task, groupId) {
1166
1279
  if (this.runningTasksService &&
1167
1280
  this.runningTasksService.getRunningTasks([task.id]).length) {
1281
+ let readyWhen;
1282
+ try {
1283
+ readyWhen = this.getReadyWhen(task);
1284
+ }
1285
+ catch (e) {
1286
+ return this.failTaskBeforeStart(task, groupId, e);
1287
+ }
1168
1288
  await this.preRunSteps([task], { groupId });
1169
1289
  if (this.tuiEnabled) {
1170
1290
  this.options.lifeCycle.setTaskStatus(task.id, 8 /* NativeTaskStatus.Shared */);
1171
1291
  }
1172
1292
  const runningTask = new shared_running_task_1.SharedRunningTask(this.runningTasksService, task.id);
1293
+ if (readyWhen) {
1294
+ const state = this.armReadiness(task.id);
1295
+ this.pollReadinessRow(task, readyWhen, state.abort.signal, true).then(() => {
1296
+ if (!state.abort.signal.aborted)
1297
+ state.resolve();
1298
+ }, (e) => {
1299
+ if (!state.abort.signal.aborted)
1300
+ this.failReadiness(task, state, e);
1301
+ });
1302
+ }
1173
1303
  this.runningContinuousTasks.set(task.id, {
1174
1304
  runningTask,
1175
1305
  groupId,
@@ -1183,34 +1313,72 @@ class TaskOrchestrator {
1183
1313
  }));
1184
1314
  // task is already running by another process, we schedule the next tasks
1185
1315
  // and release the threads
1316
+ this.tasksSchedule.markContinuousTaskStarted(task.id);
1186
1317
  await this.scheduleNextTasksAndReleaseThreads();
1187
1318
  return runningTask;
1188
1319
  }
1189
- const taskSpecificEnv = await this.processedTasks.get(task.id);
1190
- await this.preRunSteps([task], { groupId });
1191
- const pipeOutput = await this.pipeOutputCapture(task);
1192
- // obtain metadata
1193
- const temporaryOutputPath = this.cache.temporaryOutputPath(task);
1194
- // Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
1195
- // `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
1196
- // only reports at `endCommand`, which never runs for a task that does not
1197
- // end - so suppressing here would leave a `nx serve` under `summary` with a
1198
- // permanently silent terminal and no file to read yet. Streaming is the only
1199
- // channel a continuous task has.
1200
- const streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
1201
- ? false
1202
- : (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
1203
- let env = pipeOutput
1204
- ? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
1205
- : (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
1206
- this.detectTaskInvocationLoop(task);
1207
- const childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
1320
+ // Claim the task before doing any of the work below. getRunningTasks()
1321
+ // above is a check-then-act: until this row exists, a sibling Nx process
1322
+ // asking the same question is told the task is not running and starts a
1323
+ // duplicate of it.
1208
1324
  this.runningTasksService?.addRunningTask(task.id);
1209
- this.runningContinuousTasks.set(task.id, {
1210
- runningTask: childProcess,
1211
- groupId,
1212
- ownsRunningTasksService: true,
1213
- });
1325
+ let taskSpecificEnv;
1326
+ let readyWhen;
1327
+ try {
1328
+ taskSpecificEnv = await this.processedTasks.get(task.id);
1329
+ readyWhen = this.getReadyWhen(task);
1330
+ await this.waitForReadyDependencies(task);
1331
+ }
1332
+ catch (e) {
1333
+ this.runningTasksService?.removeRunningTask(task.id);
1334
+ if (e instanceof ready_when_1.ProducerFailedError) {
1335
+ await this.skipWaiter(task, groupId);
1336
+ return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: '' });
1337
+ }
1338
+ return this.failTaskBeforeStart(task, groupId, e);
1339
+ }
1340
+ // Skipped by a failed dependency's propagation while waiting
1341
+ if (this.completedTasks.has(task.id)) {
1342
+ this.runningTasksService?.removeRunningTask(task.id);
1343
+ return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: '' });
1344
+ }
1345
+ let childProcess;
1346
+ try {
1347
+ await this.preRunSteps([task], { groupId });
1348
+ const pipeOutput = await this.pipeOutputCapture(task);
1349
+ // obtain metadata
1350
+ const temporaryOutputPath = this.cache.temporaryOutputPath(task);
1351
+ // Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
1352
+ // `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
1353
+ // only reports at `endCommand`, which never runs for a task that does not
1354
+ // end - so suppressing here would leave a `nx serve` under `summary` with a
1355
+ // permanently silent terminal and no file to read yet. Streaming is the only
1356
+ // channel a continuous task has.
1357
+ const streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
1358
+ ? false
1359
+ : (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
1360
+ let env = pipeOutput
1361
+ ? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
1362
+ : (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
1363
+ this.detectTaskInvocationLoop(task);
1364
+ childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
1365
+ this.tasksSchedule.markContinuousTaskStarted(task.id);
1366
+ if (readyWhen) {
1367
+ this.startReadinessProbe(task, readyWhen, childProcess);
1368
+ }
1369
+ this.runningContinuousTasks.set(task.id, {
1370
+ runningTask: childProcess,
1371
+ groupId,
1372
+ ownsRunningTasksService: true,
1373
+ });
1374
+ }
1375
+ catch (e) {
1376
+ // Nothing owns the claim until runningContinuousTasks holds it — the
1377
+ // release paths (completeContinuousTask, the signal handler) both iterate
1378
+ // that map. Drop it here or siblings wait on a task no one is running.
1379
+ this.runningTasksService?.removeRunningTask(task.id);
1380
+ throw e;
1381
+ }
1214
1382
  this.continuousTaskExitHandled.set(task.id, new Promise((resolve) => {
1215
1383
  childProcess.onExit(async (code) => {
1216
1384
  await this.handleContinuousTaskExit(code, task, groupId, true);
@@ -1224,6 +1392,194 @@ class TaskOrchestrator {
1224
1392
  return childProcess;
1225
1393
  }
1226
1394
  // endregion Single Task
1395
+ // region Readiness
1396
+ getReadyWhen(task) {
1397
+ return (0, ready_when_1.getReadyWhen)(task, this.projectGraph, this.projects, workspace_root_1.workspaceRoot);
1398
+ }
1399
+ async waitForReadyDependencies(task) {
1400
+ const producerIds = (0, ready_when_1.getReadyProducerIds)(task, this.fullTaskGraph);
1401
+ for (const producerId of producerIds) {
1402
+ const producer = this.fullTaskGraph.tasks[producerId];
1403
+ const readyWhen = this.getReadyWhen(producer);
1404
+ if (!readyWhen) {
1405
+ if (process.env.NX_VERBOSE_LOGGING === 'true') {
1406
+ console.log(`Task "${producer.id}" declares no "readyWhen", so "${task.id}" runs once it has started.`);
1407
+ }
1408
+ continue;
1409
+ }
1410
+ // A producer in this task graph is held by dispatch and settled by its
1411
+ // probe or row poll; one outside it (an agent's flat graph) has only its row
1412
+ if (this.taskGraph.tasks[producerId]) {
1413
+ await this.readinessOf(producerId).promise;
1414
+ }
1415
+ else {
1416
+ this.onReadinessHold(producerId);
1417
+ await this.pollReadinessRow(producer, readyWhen);
1418
+ }
1419
+ }
1420
+ }
1421
+ onReadinessHold(producerId) {
1422
+ if (!this.waitingLogged.has(producerId) &&
1423
+ !this.tuiEnabled &&
1424
+ (0, output_1.printsTaskOutput)(this.resolvedOutputStyle)) {
1425
+ this.waitingLogged.add(producerId);
1426
+ console.log(`Waiting for "${producerId}" to be ready...`);
1427
+ }
1428
+ }
1429
+ // A row absent from the start means no Nx process this one can see owns the
1430
+ // producer, unless the caller already saw the owner: then it exited. A row
1431
+ // that disappears mid-wait means the producer exited.
1432
+ async pollReadinessRow(producer, readyWhen, signal, ownerObserved = false) {
1433
+ const deadline = Date.now() + readyWhen.timeout;
1434
+ let lastStatus = null;
1435
+ while (true) {
1436
+ if (signal?.aborted) {
1437
+ throw (0, ready_when_1.notReadyError)(producer.id, 'exited');
1438
+ }
1439
+ if (this.stopRequested || this.bailed) {
1440
+ throw (0, ready_when_1.notReadyError)(producer.id, 'was stopped');
1441
+ }
1442
+ const status = this.runningTasksService?.getTaskReadiness(producer.id) ?? null;
1443
+ if (status === null) {
1444
+ if (lastStatus !== null || ownerObserved) {
1445
+ throw (0, ready_when_1.notReadyError)(producer.id, 'exited');
1446
+ }
1447
+ return;
1448
+ }
1449
+ if (status !== lastStatus) {
1450
+ lastStatus = status;
1451
+ this.options.lifeCycle.setTaskReadiness?.(producer.id, status);
1452
+ }
1453
+ if (status === 1 /* TaskReadiness.Ready */) {
1454
+ return;
1455
+ }
1456
+ if (status === 2 /* TaskReadiness.Failed */) {
1457
+ throw new ready_when_1.ProducerFailedError(producer.id);
1458
+ }
1459
+ if (Date.now() >= deadline) {
1460
+ throw (0, ready_when_1.readinessTimeoutError)(producer.id, readyWhen);
1461
+ }
1462
+ await new Promise((resolve) => setTimeout(resolve, 100));
1463
+ }
1464
+ }
1465
+ startReadinessProbe(task, readyWhen, runningTask) {
1466
+ const state = this.armReadiness(task.id);
1467
+ this.options.lifeCycle.setTaskReadiness?.(task.id, 0 /* TaskReadiness.Pending */);
1468
+ (0, probes_1.waitForReadiness)(readyWhen, {
1469
+ taskId: task.id,
1470
+ runningTask,
1471
+ cwd: workspace_root_1.workspaceRoot,
1472
+ signal: state.abort.signal,
1473
+ }).then(() => {
1474
+ if (state.abort.signal.aborted)
1475
+ return;
1476
+ this.recordReadiness(task, 1 /* TaskReadiness.Ready */);
1477
+ state.resolve();
1478
+ }, (e) => {
1479
+ if (state.abort.signal.aborted)
1480
+ return;
1481
+ this.recordReadiness(task, 2 /* TaskReadiness.Failed */);
1482
+ this.failReadiness(task, state, e);
1483
+ });
1484
+ }
1485
+ // A verdict against a producer something in this run waits for fails the
1486
+ // producer, and the skip propagation covers the waiters. Without a waiter
1487
+ // the producer is left alone, as a plain `nx serve` with a probe would be.
1488
+ async failReadiness(task, state, e) {
1489
+ const message = e?.message ?? String(e);
1490
+ const entry = this.runningContinuousTasks.get(task.id);
1491
+ if (entry &&
1492
+ !(e instanceof ready_when_1.NotReadyError) &&
1493
+ this.hasReadyWaiter(task.id)) {
1494
+ if (this.tuiEnabled) {
1495
+ this.options.lifeCycle.appendTaskOutput(task.id, `\r\n${message}\r\n`, false);
1496
+ }
1497
+ else {
1498
+ console.error(message);
1499
+ }
1500
+ this.readinessFailureReasons.set(task.id, message);
1501
+ const completion = this.complete([{ task, status: 'failure', terminalOutput: message }], entry.groupId);
1502
+ this.readinessFailures.set(task.id, completion);
1503
+ try {
1504
+ await completion;
1505
+ }
1506
+ finally {
1507
+ this.readinessFailures.delete(task.id);
1508
+ }
1509
+ }
1510
+ else if (process.env.NX_VERBOSE_LOGGING === 'true') {
1511
+ console.error(message);
1512
+ }
1513
+ state.reject(e);
1514
+ }
1515
+ // On an agent the waiter runs in another process with its own flat graph
1516
+ hasReadyWaiter(producerId) {
1517
+ return Object.values(this.fullTaskGraph.tasks).some((task) => (0, ready_when_1.getReadyProducerIds)(task, this.fullTaskGraph).includes(producerId));
1518
+ }
1519
+ // The producer is not in this task graph, so no skip propagates to the waiter
1520
+ async skipWaiter(task, groupId) {
1521
+ if (!this.completedTasks.has(task.id)) {
1522
+ await this.complete([{ task, status: 'skipped' }], groupId);
1523
+ }
1524
+ }
1525
+ // The probe's verdict stands for local waiters even when the row for
1526
+ // other processes cannot be written
1527
+ recordReadiness(task, status) {
1528
+ this.options.lifeCycle.setTaskReadiness?.(task.id, status);
1529
+ try {
1530
+ this.runningTasksService?.setTaskReadiness(task.id, status);
1531
+ }
1532
+ catch (e) {
1533
+ if (process.env.NX_VERBOSE_LOGGING === 'true') {
1534
+ console.error(`Failed to record readiness of "${task.id}":`, e);
1535
+ }
1536
+ }
1537
+ }
1538
+ readinessOf(taskId) {
1539
+ return this.readiness.get(taskId) ?? this.armReadiness(taskId);
1540
+ }
1541
+ // Replaces a state settled by an earlier run so a restart re-arms it
1542
+ armReadiness(taskId) {
1543
+ let state = this.readiness.get(taskId);
1544
+ if (!state || state.settled) {
1545
+ this.tasksSchedule.markReadinessPending(taskId);
1546
+ state = createReadinessState((outcome) => {
1547
+ if (outcome === 'ready') {
1548
+ this.tasksSchedule.markReady(taskId);
1549
+ }
1550
+ else {
1551
+ this.tasksSchedule.markReadinessFailed(taskId);
1552
+ }
1553
+ this.readinessSettled = true;
1554
+ this.releaseThreads();
1555
+ });
1556
+ this.readiness.set(taskId, state);
1557
+ }
1558
+ return state;
1559
+ }
1560
+ abortReadiness(taskId, reason) {
1561
+ const state = this.readiness.get(taskId);
1562
+ if (!state)
1563
+ return;
1564
+ state.abort.abort();
1565
+ state.reject((0, ready_when_1.notReadyError)(taskId, reason));
1566
+ }
1567
+ abortAllReadiness() {
1568
+ for (const taskId of this.readiness.keys()) {
1569
+ this.abortReadiness(taskId, 'was stopped');
1570
+ }
1571
+ }
1572
+ async failTaskBeforeStart(task, groupId, e, doNotSkipCache = false) {
1573
+ this.abortReadiness(task.id, 'failed');
1574
+ // Already skipped by a failed dependency's propagation
1575
+ if (!this.completedTasks.has(task.id)) {
1576
+ // Lifecycles pair endTasks with startTasks, so start it before failing it
1577
+ await this.preRunSteps([task], { groupId });
1578
+ await this.handleDiscreteWorkerFailure(doNotSkipCache, task, groupId, e);
1579
+ }
1580
+ return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: e?.message ?? '' });
1581
+ }
1582
+ // endregion Readiness
1227
1583
  // region Lifecycle
1228
1584
  async preRunSteps(tasks, metadata) {
1229
1585
  const now = Date.now();
@@ -1364,7 +1720,13 @@ class TaskOrchestrator {
1364
1720
  if (!task.hash)
1365
1721
  return;
1366
1722
  try {
1367
- const { size } = (0, fs_1.statSync)(this.cache.temporaryOutputPath(task));
1723
+ const outputPath = this.cache.temporaryOutputPath(task);
1724
+ // The runner's exit handler wrote the child's transcript alone
1725
+ const reason = this.readinessFailureReasons.get(task.id);
1726
+ if (reason) {
1727
+ (0, fs_1.appendFileSync)(outputPath, `\n${reason}\n`);
1728
+ }
1729
+ const { size } = (0, fs_1.statSync)(outputPath);
1368
1730
  this.cache.recordTerminalOutputs([{ hash: task.hash, size }]);
1369
1731
  }
1370
1732
  catch {
@@ -1373,12 +1735,13 @@ class TaskOrchestrator {
1373
1735
  }
1374
1736
  async scheduleNextTasksAndReleaseThreads() {
1375
1737
  if (this.stopRequested) {
1376
- this.waitingForTasks.forEach((f) => f(null));
1377
- this.waitingForTasks.length = 0;
1738
+ this.releaseThreads();
1378
1739
  return;
1379
1740
  }
1380
1741
  await this.tasksSchedule.scheduleNextTasks();
1381
- // release blocked threads
1742
+ this.releaseThreads();
1743
+ }
1744
+ releaseThreads() {
1382
1745
  this.waitingForTasks.forEach((f) => f(null));
1383
1746
  this.waitingForTasks.length = 0;
1384
1747
  }
@@ -1425,8 +1788,7 @@ class TaskOrchestrator {
1425
1788
  if (this.completedTasks.has(task.id))
1426
1789
  continue;
1427
1790
  this.completedTasks.set(task.id, status);
1428
- this.taskInvocationTracker?.unregisterTask(task.id);
1429
- this.registeredInvocations.delete(task.id);
1791
+ this.releaseTaskInvocation(task);
1430
1792
  if (this.tuiEnabled) {
1431
1793
  this.options.lifeCycle.setTaskStatus(task.id, displayStatus ?? (0, native_1.parseTaskStatus)(status));
1432
1794
  }
@@ -1437,6 +1799,7 @@ class TaskOrchestrator {
1437
1799
  // mark the execution as bailed which will stop all further execution
1438
1800
  // only the tasks that are currently running will finish
1439
1801
  this.bailed = true;
1802
+ this.abortAllReadiness();
1440
1803
  }
1441
1804
  else {
1442
1805
  // Collect reverse deps to skip
@@ -1467,6 +1830,10 @@ class TaskOrchestrator {
1467
1830
  if (process.env.NX_NATIVE_COMMAND_RUNNER !== 'false') {
1468
1831
  return true;
1469
1832
  }
1833
+ // The log probe reads the captured output
1834
+ if (this.getReadyWhen(task)?.kind === 'logMatches') {
1835
+ return true;
1836
+ }
1470
1837
  // When TUI is enabled, we need to use pipe output capture to support
1471
1838
  // progressive output streaming via the onOutput callback
1472
1839
  if (this.tuiEnabled) {
@@ -1520,8 +1887,13 @@ class TaskOrchestrator {
1520
1887
  }
1521
1888
  // endregion utils
1522
1889
  async handleContinuousTaskExit(code, task, groupId, ownsRunningTasksService) {
1523
- // If cleanup already completed this task, nothing left to do
1890
+ await this.readinessFailures.get(task.id);
1891
+ // Completed by cleanup, or failed its readiness check while still running
1524
1892
  if (this.completedTasks.has(task.id)) {
1893
+ if (this.runningContinuousTasks.delete(task.id) &&
1894
+ ownsRunningTasksService) {
1895
+ this.runningTasksService?.removeRunningTask(task.id);
1896
+ }
1525
1897
  return;
1526
1898
  }
1527
1899
  const stoppingReason = this.runningContinuousTasks.get(task.id)?.stoppingReason;
@@ -1542,15 +1914,20 @@ class TaskOrchestrator {
1542
1914
  isContinuousTaskNeeded(taskId) {
1543
1915
  return this.tasksSchedule
1544
1916
  .getIncompleteTasks()
1545
- .some((t) => this.taskGraph.continuousDependencies[t.id]?.includes(taskId));
1917
+ .some((t) => this.taskGraph.continuousDependencies[t.id]?.some((edge) => edge.id === taskId));
1546
1918
  }
1547
1919
  async completeContinuousTask(task, groupId, ownsRunningTasksService, reason) {
1548
- if (this.completedTasks.has(task.id))
1549
- return;
1920
+ await this.readinessFailures.get(task.id);
1550
1921
  this.runningContinuousTasks.delete(task.id);
1551
1922
  if (ownsRunningTasksService) {
1552
1923
  this.runningTasksService?.removeRunningTask(task.id);
1553
1924
  }
1925
+ // A probe that does not need the child (url, port, command) could still
1926
+ // pass during completion and release dependents of an exited producer
1927
+ this.readiness.get(task.id)?.abort.abort();
1928
+ // Failed its readiness check while still running
1929
+ if (this.completedTasks.has(task.id))
1930
+ return;
1554
1931
  task.endTime = Date.now();
1555
1932
  if (reason === 'fulfilled') {
1556
1933
  await this.complete([
@@ -1567,6 +1944,11 @@ class TaskOrchestrator {
1567
1944
  else {
1568
1945
  await this.complete([{ task, status: 'stopped' }], groupId);
1569
1946
  }
1947
+ // After the skip propagation above, so a waiter is reported as skipped
1948
+ // rather than as failed
1949
+ this.abortReadiness(task.id, 'exited');
1950
+ // Skipped dependents leave nothing in flight to wake the loops
1951
+ this.releaseThreads();
1570
1952
  }
1571
1953
  async cleanup() {
1572
1954
  if (this.cleanupPromise) {
@@ -1576,6 +1958,8 @@ class TaskOrchestrator {
1576
1958
  return this.cleanupPromise;
1577
1959
  }
1578
1960
  async performCleanup() {
1961
+ // Waiters must not sit out a probe once the run is stopping
1962
+ this.abortAllReadiness();
1579
1963
  // Mark all running tasks for intentional stop
1580
1964
  const reason = this.stopRequested ? 'interrupted' : 'fulfilled';
1581
1965
  for (const entry of this.runningContinuousTasks.values()) {
@@ -1716,7 +2100,7 @@ class TaskOrchestrator {
1716
2100
  }
1717
2101
  const continuousDependencies = this.taskGraph.continuousDependencies[task.id];
1718
2102
  for (const continuousDependency of continuousDependencies) {
1719
- neededContinuousTasks.add(continuousDependency);
2103
+ neededContinuousTasks.add(continuousDependency.id);
1720
2104
  }
1721
2105
  }
1722
2106
  for (const [taskId, entry] of this.runningContinuousTasks) {