@ohos-ports/nx 23.3.0-beta.2 → 24.0.2-beta.0

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 (294) hide show
  1. package/README.md +3 -6
  2. package/dist/bin/nx.js +2 -2
  3. package/dist/bin/post-install.js +5 -3
  4. package/dist/nx-cloud-internals.d.ts +1 -0
  5. package/dist/nx-cloud-internals.js +7 -1
  6. package/dist/src/adapter/ngcli-adapter.js +2 -2
  7. package/dist/src/ai/clone-ai-config-repo.js +49 -12
  8. package/dist/src/command-line/add/add.d.ts +2 -0
  9. package/dist/src/command-line/add/add.js +23 -5
  10. package/dist/src/command-line/affected/affected.js +2 -2
  11. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +0 -12
  12. package/dist/src/command-line/graph/graph.js +38 -10
  13. package/dist/src/command-line/init/command-object.js +0 -12
  14. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +21 -62
  15. package/dist/src/command-line/init/init-v2.js +0 -2
  16. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +1 -1
  17. package/dist/src/command-line/migrate/agentic/close-agent-session.js +4 -3
  18. package/dist/src/command-line/migrate/agentic/definitions.d.ts +1 -3
  19. package/dist/src/command-line/migrate/agentic/definitions.js +5 -97
  20. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +7 -45
  21. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +9 -51
  22. package/dist/src/command-line/migrate/agentic/handoff.d.ts +1 -43
  23. package/dist/src/command-line/migrate/agentic/handoff.js +3 -119
  24. package/dist/src/command-line/migrate/agentic/instruction-files.d.ts +0 -21
  25. package/dist/src/command-line/migrate/agentic/instruction-files.js +6 -27
  26. package/dist/src/command-line/migrate/agentic/master/run-master-session.d.ts +19 -1
  27. package/dist/src/command-line/migrate/agentic/master/run-master-session.js +88 -49
  28. package/dist/src/command-line/migrate/agentic/master/spawn-master.d.ts +1 -0
  29. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +86 -24
  30. package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.d.ts +2 -2
  31. package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.js +2 -2
  32. package/dist/src/command-line/migrate/agentic/prompts/fragments.d.ts +1 -12
  33. package/dist/src/command-line/migrate/agentic/prompts/fragments.js +8 -17
  34. package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.d.ts +0 -15
  35. package/dist/src/command-line/migrate/agentic/prompts/shared-rendering.js +3 -100
  36. package/dist/src/command-line/migrate/agentic/select.d.ts +12 -6
  37. package/dist/src/command-line/migrate/agentic/select.js +38 -8
  38. package/dist/src/command-line/migrate/agentic/types.d.ts +9 -68
  39. package/dist/src/command-line/migrate/agentic/types.js +6 -8
  40. package/dist/src/command-line/migrate/command-object.js +6 -10
  41. package/dist/src/command-line/migrate/execute-migration.d.ts +6 -6
  42. package/dist/src/command-line/migrate/execute-migration.js +25 -15
  43. package/dist/src/command-line/migrate/migrate-analytics.d.ts +18 -1
  44. package/dist/src/command-line/migrate/migrate-analytics.js +38 -0
  45. package/dist/src/command-line/migrate/migrate-commits.d.ts +29 -17
  46. package/dist/src/command-line/migrate/migrate-commits.js +86 -32
  47. package/dist/src/command-line/migrate/migrate-config.d.ts +6 -6
  48. package/dist/src/command-line/migrate/migrate-config.js +12 -6
  49. package/dist/src/command-line/migrate/migrate-output.d.ts +1 -6
  50. package/dist/src/command-line/migrate/migrate-output.js +1 -10
  51. package/dist/src/command-line/migrate/migrate.d.ts +6 -30
  52. package/dist/src/command-line/migrate/migrate.js +257 -255
  53. package/dist/src/command-line/migrate/resolve-package-version.d.ts +2 -4
  54. package/dist/src/command-line/migrate/resolve-package-version.js +2 -35
  55. package/dist/src/command-line/migrate/run/agent-output.d.ts +2 -0
  56. package/dist/src/command-line/migrate/run/agent-output.js +9 -0
  57. package/dist/src/command-line/migrate/run/broker.d.ts +14 -5
  58. package/dist/src/command-line/migrate/run/broker.js +69 -21
  59. package/dist/src/command-line/migrate/run/clean-retry.js +4 -2
  60. package/dist/src/command-line/migrate/run/existing-run-report.d.ts +1 -1
  61. package/dist/src/command-line/migrate/run/existing-run-report.js +7 -5
  62. package/dist/src/command-line/migrate/run/give-up.d.ts +4 -3
  63. package/dist/src/command-line/migrate/run/give-up.js +8 -5
  64. package/dist/src/command-line/migrate/run/index.d.ts +7 -5
  65. package/dist/src/command-line/migrate/run/index.js +16 -1
  66. package/dist/src/command-line/migrate/run/issues.js +7 -2
  67. package/dist/src/command-line/migrate/run/orchestrator.d.ts +61 -4
  68. package/dist/src/command-line/migrate/run/orchestrator.js +397 -133
  69. package/dist/src/command-line/migrate/run/run-state.d.ts +17 -1
  70. package/dist/src/command-line/migrate/run/run-state.js +41 -2
  71. package/dist/src/command-line/migrate/run/runbook.d.ts +1 -0
  72. package/dist/src/command-line/migrate/run/runbook.js +115 -18
  73. package/dist/src/command-line/migrate/run/state-lock.d.ts +29 -4
  74. package/dist/src/command-line/migrate/run/state-lock.js +102 -10
  75. package/dist/src/command-line/migrate/run/state-machine.js +4 -1
  76. package/dist/src/command-line/migrate/run/util.d.ts +7 -2
  77. package/dist/src/command-line/migrate/run/util.js +30 -3
  78. package/dist/src/command-line/migrate/run/worker.d.ts +1 -1
  79. package/dist/src/command-line/migrate/run/worker.js +252 -250
  80. package/dist/src/command-line/migrate/sort-migrations.d.ts +1 -7
  81. package/dist/src/command-line/migrate/sort-migrations.js +1 -12
  82. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +17 -12
  83. package/dist/src/command-line/release/changelog.js +9 -6
  84. package/dist/src/command-line/release/config/config.d.ts +1 -0
  85. package/dist/src/command-line/release/config/config.js +48 -9
  86. package/dist/src/command-line/release/release.js +2 -2
  87. package/dist/src/command-line/release/utils/git.js +8 -7
  88. package/dist/src/command-line/release/utils/interpolate-release-config.d.ts +6 -0
  89. package/dist/src/command-line/release/utils/interpolate-release-config.js +39 -0
  90. package/dist/src/command-line/release/utils/release-graph.d.ts +5 -0
  91. package/dist/src/command-line/release/utils/release-graph.js +1 -0
  92. package/dist/src/command-line/release/utils/remote-release-clients/github.js +5 -6
  93. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +5 -6
  94. package/dist/src/command-line/release/utils/resolve-changelog-renderer.js +2 -2
  95. package/dist/src/command-line/release/utils/shared.js +10 -10
  96. package/dist/src/command-line/release/utils/version-plan-utils.d.ts +3 -12
  97. package/dist/src/command-line/release/utils/version-plan-utils.js +33 -55
  98. package/dist/src/command-line/release/version/version-actions.js +2 -1
  99. package/dist/src/command-line/release/version.js +5 -2
  100. package/dist/src/command-line/reset/reset.js +58 -62
  101. package/dist/src/command-line/run/executor-utils.js +1 -0
  102. package/dist/src/command-line/run/run-one.d.ts +20 -1
  103. package/dist/src/command-line/run/run-one.js +153 -5
  104. package/dist/src/command-line/run/run.js +2 -1
  105. package/dist/src/command-line/show/projects.js +5 -3
  106. package/dist/src/command-line/show/show-target/info.d.ts +10 -1
  107. package/dist/src/command-line/show/show-target/info.js +100 -15
  108. package/dist/src/command-line/show/show-target/inputs.js +10 -2
  109. package/dist/src/command-line/show/show-target/utils.js +1 -0
  110. package/dist/src/command-line/watch/command-object.js +3 -22
  111. package/dist/src/command-line/watch/watch.d.ts +0 -7
  112. package/dist/src/command-line/watch/watch.js +1 -1
  113. package/dist/src/command-line/yargs-utils/shared-options.js +1 -1
  114. package/dist/src/config/misc-interfaces.d.ts +18 -0
  115. package/dist/src/config/nx-json.d.ts +22 -11
  116. package/dist/src/config/task-graph.d.ts +1 -1
  117. package/dist/src/config/workspace-json-project-json.d.ts +88 -0
  118. package/dist/src/core/graph/main.js +1 -1
  119. package/dist/src/core/graph/runtime.js +1 -1
  120. package/dist/src/core/graph/styles.css +1 -1
  121. package/dist/src/core/graph/styles.js +1 -1
  122. package/dist/src/daemon/client/client.d.ts +15 -0
  123. package/dist/src/daemon/client/client.js +181 -71
  124. package/dist/src/daemon/client/daemon-socket-messenger.d.ts +1 -1
  125. package/dist/src/daemon/client/daemon-socket-messenger.js +2 -2
  126. package/dist/src/daemon/server/client-socket-context.js +1 -1
  127. package/dist/src/daemon/server/handle-process-in-background.js +21 -9
  128. package/dist/src/daemon/server/handle-select-affected-tasks.d.ts +2 -1
  129. package/dist/src/daemon/server/handle-select-affected-tasks.js +21 -13
  130. package/dist/src/daemon/server/native-js-loader.d.ts +1 -0
  131. package/dist/src/daemon/server/native-js-loader.js +21 -0
  132. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +4 -8
  133. package/dist/src/daemon/server/server.d.ts +2 -1
  134. package/dist/src/daemon/server/server.js +17 -4
  135. package/dist/src/daemon/server/shutdown-utils.js +0 -3
  136. package/dist/src/daemon/server/start.d.ts +1 -0
  137. package/dist/src/daemon/server/start.js +2 -0
  138. package/dist/src/daemon/socket-utils.d.ts +11 -9
  139. package/dist/src/daemon/socket-utils.js +31 -61
  140. package/dist/src/daemon/tmp-dir.d.ts +19 -14
  141. package/dist/src/daemon/tmp-dir.js +142 -354
  142. package/dist/src/devkit-exports.d.ts +9 -1
  143. package/dist/src/devkit-exports.js +12 -1
  144. package/dist/src/devkit-internals.d.ts +6 -5
  145. package/dist/src/devkit-internals.js +13 -4
  146. package/dist/src/executors/run-commands/run-commands.impl.d.ts +4 -2
  147. package/dist/src/executors/run-commands/run-commands.impl.js +53 -27
  148. package/dist/src/executors/run-commands/running-tasks.d.ts +13 -4
  149. package/dist/src/executors/run-commands/running-tasks.js +148 -95
  150. package/dist/src/executors/run-commands/schema.json +1 -1
  151. package/dist/src/hasher/check-task-files.js +5 -8
  152. package/dist/src/hasher/file-hasher.js +9 -7
  153. package/dist/src/hasher/hash-task.js +4 -0
  154. package/dist/src/hasher/native-task-hasher-impl.js +3 -1
  155. package/dist/src/hasher/task-hasher.d.ts +2 -0
  156. package/dist/src/hasher/task-hasher.js +4 -1
  157. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.d.ts +1 -2
  158. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +6 -11
  159. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.md +2 -2
  160. package/dist/src/migrations/update-24-0-0/enable-legacy-affected.d.ts +4 -0
  161. package/dist/src/migrations/update-24-0-0/enable-legacy-affected.js +15 -0
  162. package/dist/src/migrations/update-24-0-0/enable-legacy-affected.md +30 -0
  163. package/dist/src/migrations/update-24-0-0/find-custom-hashers.d.ts +3 -0
  164. package/dist/src/migrations/update-24-0-0/find-custom-hashers.js +100 -0
  165. package/dist/src/migrations/update-24-0-0/find-custom-hashers.md +63 -0
  166. package/dist/src/migrations/update-24-0-0/move-custom-hashers-to-inputs.md +146 -0
  167. package/dist/src/migrations/update-24-0-0/rename-ultracache-ignore-options.d.ts +3 -0
  168. package/dist/src/migrations/update-24-0-0/rename-ultracache-ignore-options.js +130 -0
  169. package/dist/src/migrations/update-24-0-0/rename-ultracache-ignore-options.md +62 -0
  170. package/dist/src/native/assert-supported-platform.js +1 -1
  171. package/dist/src/native/index.d.ts +238 -18
  172. package/dist/src/native/index.js +6 -1
  173. package/dist/src/native/native-bindings.js +15 -6
  174. package/dist/src/native/nx.openharmony-arm64.node +0 -0
  175. package/dist/src/native/nx.wasi-browser.js +1 -0
  176. package/dist/src/native/nx.wasi.cjs +1 -0
  177. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  178. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  179. package/dist/src/nx-cloud/update-manager.d.ts +14 -0
  180. package/dist/src/nx-cloud/update-manager.js +237 -50
  181. package/dist/src/plugins/js/lock-file/pnpm-parser.js +3 -4
  182. package/dist/src/plugins/js/lock-file/pruned-output.d.ts +2 -2
  183. package/dist/src/plugins/js/lock-file/pruned-output.js +4 -4
  184. package/dist/src/plugins/js/utils/register.js +9 -4
  185. package/dist/src/project-graph/affected/affected-tasks.d.ts +9 -6
  186. package/dist/src/project-graph/affected/affected-tasks.js +31 -7
  187. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +7 -0
  188. package/dist/src/project-graph/plugins/loaded-nx-plugin.js +49 -6
  189. package/dist/src/project-graph/utils/project-configuration/target-merging.js +1 -1
  190. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +12 -133
  191. package/dist/src/project-graph/utils/project-configuration-utils.js +2 -2
  192. package/dist/src/project-graph/utils/retrieve-workspace-files.js +27 -6
  193. package/dist/src/tasks-runner/create-task-graph.d.ts +8 -7
  194. package/dist/src/tasks-runner/create-task-graph.js +50 -27
  195. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  196. package/dist/src/tasks-runner/init-tasks-runner.js +116 -59
  197. package/dist/src/tasks-runner/life-cycle.d.ts +4 -1
  198. package/dist/src/tasks-runner/life-cycle.js +7 -0
  199. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +5 -2
  200. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +13 -7
  201. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +4 -3
  202. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +9 -2
  203. package/dist/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.js +5 -2
  204. package/dist/src/tasks-runner/life-cycles/static-run-one-terminal-output-life-cycle.js +4 -1
  205. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.js +5 -1
  206. package/dist/src/tasks-runner/pseudo-ipc.d.ts +1 -0
  207. package/dist/src/tasks-runner/pseudo-ipc.js +4 -0
  208. package/dist/src/tasks-runner/pseudo-terminal.d.ts +1 -1
  209. package/dist/src/tasks-runner/pseudo-terminal.js +2 -2
  210. package/dist/src/tasks-runner/readiness/executor-ready-signal.d.ts +7 -0
  211. package/dist/src/tasks-runner/readiness/executor-ready-signal.js +43 -0
  212. package/dist/src/tasks-runner/readiness/probes.d.ts +14 -0
  213. package/dist/src/tasks-runner/readiness/probes.js +101 -0
  214. package/dist/src/tasks-runner/readiness/ready-signal-server.d.ts +18 -0
  215. package/dist/src/tasks-runner/readiness/ready-signal-server.js +72 -0
  216. package/dist/src/tasks-runner/readiness/ready-signal.d.ts +33 -0
  217. package/dist/src/tasks-runner/readiness/ready-signal.js +92 -0
  218. package/dist/src/tasks-runner/readiness/ready-when.d.ts +84 -0
  219. package/dist/src/tasks-runner/readiness/ready-when.js +371 -0
  220. package/dist/src/tasks-runner/readiness/startup-host.d.ts +1 -0
  221. package/dist/src/tasks-runner/readiness/startup-host.js +32 -0
  222. package/dist/src/tasks-runner/run-command.d.ts +6 -2
  223. package/dist/src/tasks-runner/run-command.js +21 -31
  224. package/dist/src/tasks-runner/run-outcome.d.ts +16 -0
  225. package/dist/src/tasks-runner/run-outcome.js +39 -0
  226. package/dist/src/tasks-runner/running-tasks/batch-process.js +6 -0
  227. package/dist/src/tasks-runner/running-tasks/node-child-process.js +5 -4
  228. package/dist/src/tasks-runner/running-tasks/running-task.d.ts +1 -1
  229. package/dist/src/tasks-runner/task-env.d.ts +10 -0
  230. package/dist/src/tasks-runner/task-env.js +37 -5
  231. package/dist/src/tasks-runner/task-graph-utils.d.ts +9 -6
  232. package/dist/src/tasks-runner/task-graph-utils.js +17 -12
  233. package/dist/src/tasks-runner/task-orchestrator.d.ts +62 -18
  234. package/dist/src/tasks-runner/task-orchestrator.js +750 -150
  235. package/dist/src/tasks-runner/tasks-runner.d.ts +1 -1
  236. package/dist/src/tasks-runner/tasks-schedule.d.ts +18 -1
  237. package/dist/src/tasks-runner/tasks-schedule.js +63 -6
  238. package/dist/src/tasks-runner/tokens.d.ts +54 -0
  239. package/dist/src/tasks-runner/tokens.js +161 -0
  240. package/dist/src/tasks-runner/utils.d.ts +35 -6
  241. package/dist/src/tasks-runner/utils.js +149 -68
  242. package/dist/src/ultracache/ci-status.d.ts +24 -0
  243. package/dist/src/ultracache/ci-status.js +87 -0
  244. package/dist/src/ultracache/config.d.ts +3 -3
  245. package/dist/src/ultracache/config.js +4 -4
  246. package/dist/src/ultracache/fetch.d.ts +2 -0
  247. package/dist/src/ultracache/fetch.js +1 -0
  248. package/dist/src/ultracache/overrides.js +4 -0
  249. package/dist/src/utils/acknowledge-build-scripts.d.ts +34 -1
  250. package/dist/src/utils/acknowledge-build-scripts.js +171 -32
  251. package/dist/src/utils/child-process.js +2 -1
  252. package/dist/src/utils/find-matching-projects.js +9 -6
  253. package/dist/src/utils/git-utils.d.ts +12 -5
  254. package/dist/src/utils/git-utils.js +69 -11
  255. package/dist/src/utils/long-running-target.d.ts +1 -3
  256. package/dist/src/utils/long-running-target.js +1 -3
  257. package/dist/src/utils/output.d.ts +4 -3
  258. package/dist/src/utils/output.js +4 -3
  259. package/dist/src/utils/owned-private-dir.d.ts +1 -17
  260. package/dist/src/utils/owned-private-dir.js +10 -62
  261. package/dist/src/utils/package-manager.d.ts +8 -0
  262. package/dist/src/utils/package-manager.js +65 -8
  263. package/dist/src/utils/perf-hooks.d.ts +1 -0
  264. package/dist/src/utils/plugins/core-plugins.js +5 -1
  265. package/dist/src/utils/progress-topics.d.ts +1 -0
  266. package/dist/src/utils/progress-topics.js +1 -0
  267. package/dist/src/utils/safe-spawn.js +1 -5
  268. package/dist/src/utils/shell-quoting.d.ts +8 -1
  269. package/dist/src/utils/shell-quoting.js +12 -1
  270. package/dist/src/utils/spawn-without-terminal.d.ts +27 -0
  271. package/dist/src/utils/spawn-without-terminal.js +110 -0
  272. package/dist/src/utils/string-similarity.d.ts +38 -0
  273. package/dist/src/utils/string-similarity.js +108 -0
  274. package/migrations.json +20 -6
  275. package/package.json +15 -24
  276. package/schemas/migrations-schema.json +11 -0
  277. package/schemas/nx-schema.json +133 -11
  278. package/schemas/project-schema.json +114 -4
  279. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.d.ts +0 -56
  280. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.js +0 -67
  281. package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.d.ts +0 -49
  282. package/dist/src/command-line/migrate/agentic/prompts/hybrid-prompt-migration.js +0 -53
  283. package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.d.ts +0 -26
  284. package/dist/src/command-line/migrate/agentic/prompts/prompt-migration.js +0 -27
  285. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.d.ts +0 -70
  286. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.js +0 -113
  287. package/dist/src/command-line/migrate/agentic/run-step.d.ts +0 -68
  288. package/dist/src/command-line/migrate/agentic/run-step.js +0 -148
  289. package/dist/src/command-line/migrate/agentic/runner.d.ts +0 -21
  290. package/dist/src/command-line/migrate/agentic/runner.js +0 -262
  291. package/dist/src/migrations/update-23-0-0/consolidate-release-tag-config.d.ts +0 -9
  292. package/dist/src/migrations/update-23-0-0/consolidate-release-tag-config.js +0 -18
  293. package/dist/src/tasks-runner/legacy-depends-on-warning.d.ts +0 -18
  294. package/dist/src/tasks-runner/legacy-depends-on-warning.js +0 -109
@@ -22,6 +22,10 @@ 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_signal_1 = require("./readiness/ready-signal");
27
+ const ready_signal_server_1 = require("./readiness/ready-signal-server");
28
+ const ready_when_1 = require("./readiness/ready-when");
25
29
  const noop_child_process_1 = require("./running-tasks/noop-child-process");
26
30
  const output_prefix_1 = require("./running-tasks/output-prefix");
27
31
  const shared_running_task_1 = require("./running-tasks/shared-running-task");
@@ -36,6 +40,28 @@ const utils_1 = require("./utils");
36
40
  function resolveBatchTaskStatus(result) {
37
41
  return result.status ?? (result.success ? 'success' : 'failure');
38
42
  }
43
+ function batchWorkerLogExplains(status) {
44
+ switch (status) {
45
+ case 'failure':
46
+ case 'stopped':
47
+ // The worker never reached it, e.g. `@nx/gradle` after a build failure
48
+ // outside the batch's own tasks
49
+ case 'skipped':
50
+ return true;
51
+ case 'success':
52
+ case 'local-cache':
53
+ case 'local-cache-kept-existing':
54
+ case 'remote-cache':
55
+ return false;
56
+ default: {
57
+ const unhandled = status;
58
+ void unhandled;
59
+ // Batch executors report their statuses untyped, so an unknown one can
60
+ // still arrive here. Nx can't tell it succeeded, so its log is shown
61
+ return true;
62
+ }
63
+ }
64
+ }
39
65
  /** Whether a path names a file with bytes in it. */
40
66
  function hasContent(path) {
41
67
  try {
@@ -45,6 +71,31 @@ function hasContent(path) {
45
71
  return false;
46
72
  }
47
73
  }
74
+ function createReadinessState(onSettled) {
75
+ const state = {
76
+ settled: false,
77
+ abort: new AbortController(),
78
+ };
79
+ state.promise = new Promise((resolve, reject) => {
80
+ state.resolve = () => {
81
+ if (state.settled)
82
+ return;
83
+ state.settled = true;
84
+ resolve();
85
+ onSettled('ready');
86
+ };
87
+ state.reject = (error) => {
88
+ if (state.settled)
89
+ return;
90
+ state.settled = true;
91
+ reject(error);
92
+ onSettled('failed');
93
+ };
94
+ });
95
+ // A producer with no waiter still settles; that must not be unhandled
96
+ state.promise.catch(() => { });
97
+ return state;
98
+ }
48
99
  class TaskOrchestrator {
49
100
  // endregion internal state
50
101
  constructor(hasher, initiatingProject, initiatingTasks, projectGraph, taskGraph, nxJson, options, bail, daemon,
@@ -53,7 +104,7 @@ class TaskOrchestrator {
53
104
  /** What this run renders with, after defaults. */
54
105
  resolvedOutputStyle, fullTaskGraph = taskGraph,
55
106
  /** Tasks the up-front pass left to hash once the tasks they read from have run. */
56
- deferredTaskIds) {
107
+ hashingDeferredTaskIds) {
57
108
  this.hasher = hasher;
58
109
  this.initiatingProject = initiatingProject;
59
110
  this.initiatingTasks = initiatingTasks;
@@ -66,7 +117,7 @@ class TaskOrchestrator {
66
117
  this.specifiedOutputStyle = specifiedOutputStyle;
67
118
  this.resolvedOutputStyle = resolvedOutputStyle;
68
119
  this.fullTaskGraph = fullTaskGraph;
69
- this.deferredTaskIds = deferredTaskIds;
120
+ this.hashingDeferredTaskIds = hashingDeferredTaskIds;
70
121
  this.taskDetails = (0, hash_task_1.getTaskDetails)();
71
122
  this.cache = (0, cache_1.getCache)(this.options);
72
123
  this.tuiEnabled = (0, is_tui_enabled_1.isTuiEnabled)();
@@ -78,13 +129,17 @@ class TaskOrchestrator {
78
129
  ? new native_1.RunningTasksService((0, db_connection_1.getLocalDbConnection)())
79
130
  : null;
80
131
  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))
132
+ ? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(), (0, task_env_1.getInvocationRootPid)(), (0, task_env_1.getInvocationAncestorPids)())
82
133
  : 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.
134
+ // Task ids this process has already registered. Recursive code paths (e.g.
135
+ // applyFromCacheOrRunBatch looping on incomplete batches) re-enter the
136
+ // detector for the same task; skipping them saves a DB round trip. A task id
137
+ // is unique within one process, so it is enough to identify the row.
86
138
  this.registeredInvocations = new Set();
87
- this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options);
139
+ this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options, this.fullTaskGraph, {
140
+ readinessElsewhere: (producerId) => this.runningTasksService?.getTaskReadiness(producerId) ?? null,
141
+ onReadinessHold: (producerId) => this.onReadinessHold(producerId),
142
+ });
88
143
  // region internal state
89
144
  this.batchEnv = (0, task_env_1.getEnvVariablesForBatchProcess)(this.options.skipNxCache, this.options.captureStderr);
90
145
  this.reverseTaskDeps = (0, utils_1.calculateReverseDeps)(this.taskGraph);
@@ -123,6 +178,16 @@ class TaskOrchestrator {
123
178
  this.runningDiscreteTasks = new Map();
124
179
  this.discreteTaskExitHandled = new Map();
125
180
  this.continuousTaskExitHandled = new Map();
181
+ // Keyed by producer id: a producer this process started or shares
182
+ this.readiness = new Map();
183
+ // Producers that failed their check while still running: the exit must not
184
+ // report them again, and the saved log gets the reason after the transcript
185
+ this.readinessFailures = new Map();
186
+ this.readinessFailureReasons = new Map();
187
+ this.readinessSettled = false;
188
+ // The url each producer reported with its readiness signal
189
+ this.readyUrls = new Map();
190
+ this.waitingLogged = new Set();
126
191
  this.cleanupPromise = null;
127
192
  this.signalHandlers = [];
128
193
  // Tasks whose runner already owns the write to
@@ -210,8 +275,22 @@ class TaskOrchestrator {
210
275
  */
211
276
  async executeCoordinatorLoop(doNotSkipCache, parallelism) {
212
277
  while (true) {
213
- if (this.bailed || this.stopRequested)
278
+ if (this.stopRequested)
279
+ break;
280
+ // On bail, start nothing new but let running tasks finish. Skip unstarted
281
+ // discrete tasks unless stopped, so the run still reads as interrupted
282
+ if (this.bailed) {
283
+ await Promise.allSettled(this.pendingDiscreteWorkers);
284
+ if (this.stopRequested)
285
+ break;
286
+ for (const task of Object.values(this.taskGraph.tasks)) {
287
+ if (!task.continuous && !this.completedTasks.has(task.id)) {
288
+ this.completedTasks.set(task.id, 'skipped');
289
+ }
290
+ }
214
291
  break;
292
+ }
293
+ await this.scheduleAfterReadiness();
215
294
  // 1. Hash BEFORE processAll so processTask sees hashes set, and so
216
295
  // resolveCachedTasksBulk can look them up in the cache. Each task
217
296
  // is hashed with its own task-specific env (project/target .env
@@ -223,7 +302,7 @@ class TaskOrchestrator {
223
302
  const unhashed = scheduledTasks
224
303
  .map((id) => this.taskGraph.tasks[id])
225
304
  .filter((t) => !t.hash &&
226
- this.taskGraph.dependencies[t.id].every((depId) => this.completedTasks.has(depId)));
305
+ this.taskGraph.dependencies[t.id].every(({ id }) => this.completedTasks.has(id)));
227
306
  if (unhashed.length > 0) {
228
307
  const perTaskEnvs = {};
229
308
  for (const task of unhashed) {
@@ -238,6 +317,11 @@ class TaskOrchestrator {
238
317
  const resolved = await this.resolveCachedTasksBulk();
239
318
  if (resolved)
240
319
  continue;
320
+ // A pass from a continuous loop during the waits above may have
321
+ // admitted tasks this iteration never looked up; they must not reach
322
+ // step 5 without a confirmed miss
323
+ if (this.hasUnresolvedCacheableTask())
324
+ continue;
241
325
  }
242
326
  // 3. Process remaining scheduled tasks (cache misses + non-cacheable).
243
327
  this.processAllScheduledTasks();
@@ -249,6 +333,9 @@ class TaskOrchestrator {
249
333
  this.openGroup(groupId);
250
334
  continue;
251
335
  }
336
+ // A task may have failed during one of the awaits above
337
+ if (this.bailed)
338
+ continue;
252
339
  // 5. Dispatch cache misses as individual workers
253
340
  while (this.pendingDiscreteWorkers.size < parallelism) {
254
341
  const task = this.tasksSchedule.nextTask((t) => !t.continuous);
@@ -263,6 +350,10 @@ class TaskOrchestrator {
263
350
  break;
264
351
  }
265
352
  // 7. Wait for a worker to finish (woken by scheduleNextTasksAndReleaseThreads)
353
+ // A verdict that landed during this iteration found nothing parked,
354
+ // so run its pass instead of parking
355
+ if (this.readinessSettled)
356
+ continue;
266
357
  await new Promise((res) => this.waitingForTasks.push(res));
267
358
  }
268
359
  }
@@ -272,6 +363,7 @@ class TaskOrchestrator {
272
363
  if (!this.tasksSchedule.hasTasks() || this.bailed || this.stopRequested) {
273
364
  return null;
274
365
  }
366
+ await this.scheduleAfterReadiness();
275
367
  this.processAllScheduledTasks();
276
368
  const task = this.tasksSchedule.nextTask((t) => t.continuous);
277
369
  if (task) {
@@ -293,9 +385,29 @@ class TaskOrchestrator {
293
385
  return null;
294
386
  }
295
387
  // block until some other task completes, then try again
388
+ if (this.readinessSettled)
389
+ continue;
296
390
  await new Promise((res) => this.waitingForTasks.push(res));
297
391
  }
298
392
  }
393
+ hasUnresolvedCacheableTask() {
394
+ return this.tasksSchedule
395
+ .getAllScheduledTasks()
396
+ .scheduledTasks.some((id) => {
397
+ const task = this.taskGraph.tasks[id];
398
+ return (!task.continuous &&
399
+ task.cache &&
400
+ (!task.hash || !this.cacheMissedHashes.has(task.hash)));
401
+ });
402
+ }
403
+ // A verdict releases tasks the schedule has not admitted yet. Whichever
404
+ // loop wakes first runs the pass; the coordinator may be inside a batch.
405
+ async scheduleAfterReadiness() {
406
+ if (this.readinessSettled) {
407
+ this.readinessSettled = false;
408
+ await this.scheduleNextTasksAndReleaseThreads();
409
+ }
410
+ }
299
411
  // region Processing Scheduled Tasks
300
412
  async processTask(taskId) {
301
413
  const task = this.taskGraph.tasks[taskId];
@@ -308,7 +420,10 @@ class TaskOrchestrator {
308
420
  }
309
421
  processAllScheduledTasks() {
310
422
  const { scheduledTasks } = this.tasksSchedule.getAllScheduledTasks();
311
- for (const taskId of scheduledTasks) {
423
+ this.processTasks(scheduledTasks);
424
+ }
425
+ processTasks(taskIds) {
426
+ for (const taskId of taskIds) {
312
427
  // Task is already handled or being handled
313
428
  if (!this.processedTasks.has(taskId)) {
314
429
  this.processedTasks.set(taskId, this.processTask(taskId));
@@ -316,39 +431,59 @@ class TaskOrchestrator {
316
431
  }
317
432
  }
318
433
  /**
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.
434
+ * Registers a task invocation and checks for loops across nested Nx
435
+ * processes. registerTask() returns the invocation chain only when an
436
+ * *ancestor* Nx process is already running this task — a genuine loop.
437
+ * Sibling processes running the same task are legitimate and register
438
+ * without complaint.
439
+ *
440
+ * The overrides hash rides along with the task id so the tracker can tell an
441
+ * ancestor re-invoking a task with different arguments from one repeating
442
+ * the same invocation.
323
443
  */
324
444
  detectTaskInvocationLoop(task) {
325
445
  if (!this.taskInvocationTracker)
326
446
  return;
327
447
  if (this.registeredInvocations.has(task.id))
328
448
  return;
449
+ let chain;
329
450
  try {
330
- this.taskInvocationTracker.registerTask(process.pid, task.id);
451
+ chain = this.taskInvocationTracker.registerTask(process.pid, task.id, (0, utils_1.hashTaskOverrides)(task));
452
+ }
453
+ catch {
454
+ // Loop detection is diagnostic only; a DB failure must not fail the run
455
+ // or be mistaken for a loop.
456
+ return;
457
+ }
458
+ if (!chain) {
331
459
  this.registeredInvocations.add(task.id);
460
+ return;
461
+ }
462
+ const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
463
+ output_1.output.error({
464
+ title: 'Recursive task invocation detected',
465
+ bodyLines: [
466
+ `Nx detected a recursive loop of task invocations:`,
467
+ ``,
468
+ ` ${chainDisplay} -> ${task.id}`,
469
+ ``,
470
+ `Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
471
+ `This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
472
+ `triggers a chain of tasks that eventually re-invokes itself.`,
473
+ ``,
474
+ `To fix this, review the command configuration for the tasks in the chain above.`,
475
+ ],
476
+ });
477
+ process.exit(1);
478
+ }
479
+ releaseTaskInvocation(task) {
480
+ if (!this.registeredInvocations.delete(task.id))
481
+ return;
482
+ try {
483
+ this.taskInvocationTracker?.unregisterTask(process.pid, task.id);
332
484
  }
333
485
  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);
486
+ // Diagnostic only, like registration. A leftover row is swept as stale.
352
487
  }
353
488
  }
354
489
  // endregion Processing Scheduled Tasks
@@ -483,38 +618,63 @@ class TaskOrchestrator {
483
618
  /**
484
619
  * Hash all batch tasks and resolve cache hits topologically.
485
620
  *
486
- * Walks the task graph level by level. Every task gets a preliminary hash
487
- * (so startTasks always has a valid hash for Cloud). Tasks the up-front
488
- * pass deferred, whose deps weren't cached, are ineligible for cache lookup but still
489
- * receive a preliminary hash — they'll be re-hashed after execution.
621
+ * Walks the task graph level by level. A deferred task (its hash reads
622
+ * another task's outputs) behind an upstream batch task that still has to
623
+ * run is never looked up yet:
624
+ * - behind a non-cacheable task only, it is held back for a later wave,
625
+ * where it hashes after that task ran;
626
+ * - behind a cache miss, it runs in this wave and re-hashes after execution.
627
+ * Every task run in this wave gets a hash first, for Cloud's startTasks.
490
628
  */
491
629
  async applyBatchCachedResults(batch, doNotSkipCache, groupId) {
492
630
  const cachedResults = [];
493
- const needsRehashAfterExecution = new Set();
631
+ const rehashAfterRun = new Set();
494
632
  const tasks = Object.values(batch.taskGraph.tasks);
495
633
  if (!doNotSkipCache) {
496
634
  // Cache skipped — just hash so startTasks has valid hashes
497
635
  await this.hashBatchTasks(tasks);
498
- return { cachedResults, needsRehashAfterExecution };
499
- }
500
- const nonCachedTaskIds = new Set();
501
- await (0, task_graph_utils_1.walkTaskGraph)(batch.taskGraph, async (rootTaskIds) => {
502
- const rootTasks = rootTaskIds.map((id) => batch.taskGraph.tasks[id]);
503
- await this.hashBatchTasks(rootTasks);
504
- const eligible = [];
505
- for (const task of rootTasks) {
506
- const depIds = batch.taskGraph.dependencies[task.id];
507
- const hasNonCachedDep = depIds.some((id) => nonCachedTaskIds.has(id));
508
- if (hasNonCachedDep && this.deferredTaskIds?.has(task.id)) {
509
- nonCachedTaskIds.add(task.id);
510
- needsRehashAfterExecution.add(task.id);
636
+ return { cachedResults, tasksToRun: tasks, rehashAfterRun };
637
+ }
638
+ const toRun = new Map();
639
+ // Neither restored nor run in this wave; a later wave picks them up.
640
+ const heldBack = new Set();
641
+ // Tasks with a cache miss / a non-cacheable task upstream that still has
642
+ // to run in this wave.
643
+ const behindMiss = new Set();
644
+ const behindUncacheable = new Set();
645
+ const pending = (id, cacheable) => toRun.has(id) && !!toRun.get(id).cache === cacheable;
646
+ await (0, task_graph_utils_1.walkTaskGraph)(batch.taskGraph, async (levelIds) => {
647
+ const runNow = [];
648
+ const lookUp = [];
649
+ for (const id of levelIds) {
650
+ const task = batch.taskGraph.tasks[id];
651
+ const deps = batch.taskGraph.dependencies[id].map((dep) => dep.id);
652
+ if (deps.some((dep) => heldBack.has(dep))) {
653
+ heldBack.add(id);
654
+ continue;
655
+ }
656
+ const afterMiss = deps.some((dep) => behindMiss.has(dep) || pending(dep, true));
657
+ const afterUncacheable = deps.some((dep) => behindUncacheable.has(dep) || pending(dep, false));
658
+ if (afterMiss)
659
+ behindMiss.add(id);
660
+ if (afterUncacheable)
661
+ behindUncacheable.add(id);
662
+ const deferred = this.hashingDeferredTaskIds?.has(id);
663
+ if (deferred && afterMiss) {
664
+ runNow.push(task);
665
+ toRun.set(id, task);
666
+ rehashAfterRun.add(id);
667
+ }
668
+ else if (deferred && afterUncacheable) {
669
+ heldBack.add(id);
511
670
  }
512
671
  else {
513
- eligible.push(task);
672
+ lookUp.push(task);
514
673
  }
515
674
  }
516
- if (eligible.length > 0) {
517
- const cacheResults = await this.applyCachedResults(eligible);
675
+ await this.hashBatchTasks([...runNow, ...lookUp]);
676
+ if (lookUp.length > 0) {
677
+ const cacheResults = await this.applyCachedResults(lookUp);
518
678
  const cachedIds = new Set(cacheResults.map((r) => r.task.id));
519
679
  cachedResults.push(...cacheResults);
520
680
  if (cacheResults.length > 0) {
@@ -524,14 +684,14 @@ class TaskOrchestrator {
524
684
  // Replayed from the cache — don't write the results back.
525
685
  await this.postRunSteps(cacheResults, false, groupId);
526
686
  }
527
- for (const task of eligible) {
687
+ for (const task of lookUp) {
528
688
  if (!cachedIds.has(task.id)) {
529
- nonCachedTaskIds.add(task.id);
689
+ toRun.set(task.id, task);
530
690
  }
531
691
  }
532
692
  }
533
693
  });
534
- return { cachedResults, needsRehashAfterExecution };
694
+ return { cachedResults, tasksToRun: [...toRun.values()], rehashAfterRun };
535
695
  }
536
696
  async hashBatchTasks(tasks) {
537
697
  // Batch executors run every task in the same forked process, but
@@ -545,27 +705,52 @@ class TaskOrchestrator {
545
705
  await (0, hash_task_1.hashTasks)(this.hasher, this.projectGraph, this.fullTaskGraph, perTaskEnvs, this.taskDetails, tasks);
546
706
  }
547
707
  async applyFromCacheOrRunBatch(doNotSkipCache, batch, groupId) {
708
+ if (this.bailed || this.stopRequested) {
709
+ return [];
710
+ }
548
711
  const applyFromCacheOrRunBatchStart = perf_hooks_1.performance.mark('TaskOrchestrator-apply-from-cache-or-run-batch:start');
549
- const taskEntries = Object.entries(batch.taskGraph.tasks);
550
- const tasks = taskEntries.map(([, task]) => task);
712
+ const taskIds = Object.keys(batch.taskGraph.tasks);
551
713
  this.options.lifeCycle.registerRunningBatch?.(batch.id, {
552
714
  executorName: batch.executorName,
553
- taskIds: Object.keys(batch.taskGraph.tasks),
715
+ taskIds,
554
716
  });
555
- 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));
560
- if (nonCachedTasks.length > 0) {
561
- await Promise.all(nonCachedTasks.map((task) => this.options.lifeCycle.scheduleTask(task)));
562
- await this.preRunSteps(nonCachedTasks, { groupId });
563
- }
564
- // Phase 2: Run non-cached tasks, then re-hash tasks that read their outputs
565
- const taskIdsToSkip = cachedResults.map((r) => r.task.id);
717
+ // Each wave restores or runs what it can; tasks held back for a later
718
+ // wave (or left out by the executor) stay incomplete and go again.
719
+ const results = [];
720
+ let taskGraph = batch.taskGraph;
721
+ while (Object.keys(taskGraph.tasks).length > 0 &&
722
+ !this.bailed &&
723
+ !this.stopRequested) {
724
+ results.push(...(await this.runBatchWave(doNotSkipCache, { ...batch, taskGraph }, groupId)));
725
+ this.forkedProcessTaskRunner.cleanUpBatchProcesses();
726
+ taskGraph = (0, utils_1.removeTasksFromTaskGraph)(taskGraph, Object.keys(taskGraph.tasks).filter((id) => this.completedTasks.has(id)));
727
+ }
728
+ // Once, after every wave: an earlier status would end the batch in the TUI.
729
+ const hasFailures = taskIds.some((taskId) => {
730
+ const status = this.completedTasks.get(taskId);
731
+ return status === 'failure' || status === 'skipped';
732
+ });
733
+ this.options.lifeCycle.setBatchStatus?.(batch.id, hasFailures ? "Failure" /* BatchStatus.Failure */ : "Success" /* BatchStatus.Success */);
734
+ const applyFromCacheOrRunBatchEnd = perf_hooks_1.performance.mark('TaskOrchestrator-apply-from-cache-or-run-batch:end');
735
+ perf_hooks_1.performance.measure('TaskOrchestrator-apply-from-cache-or-run-batch', applyFromCacheOrRunBatchStart.name, applyFromCacheOrRunBatchEnd.name);
736
+ return results;
737
+ }
738
+ async runBatchWave(doNotSkipCache, batch, groupId) {
739
+ const tasks = Object.values(batch.taskGraph.tasks);
740
+ const { cachedResults, tasksToRun: wave, rehashAfterRun, } = await this.applyBatchCachedResults(batch, doNotSkipCache, groupId);
741
+ // Cached tasks were started and completed inside applyBatchCachedResults;
742
+ // a member skipped meanwhile (a dependency failed) must not run either
743
+ const tasksToRun = wave.filter((t) => !this.completedTasks.has(t.id));
744
+ if (tasksToRun.length > 0) {
745
+ await Promise.all(tasksToRun.map((task) => this.options.lifeCycle.scheduleTask(task)));
746
+ await this.preRunSteps(tasksToRun, { groupId });
747
+ }
748
+ // Phase 2: Run this wave's tasks, then re-hash those run without a lookup
566
749
  let batchResults = [];
567
- if (taskIdsToSkip.length < tasks.length) {
568
- const runGraph = (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, taskIdsToSkip);
750
+ // Read again: a member can be skipped while the hooks above ran
751
+ const runIds = new Set(tasksToRun.filter((t) => !this.completedTasks.has(t.id)).map((t) => t.id));
752
+ if (runIds.size > 0) {
753
+ const runGraph = (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, tasks.filter((t) => !runIds.has(t.id)).map((t) => t.id));
569
754
  for (const task of Object.values(runGraph.tasks)) {
570
755
  this.detectTaskInvocationLoop(task);
571
756
  }
@@ -576,7 +761,7 @@ class TaskOrchestrator {
576
761
  }, this.batchEnv, groupId);
577
762
  // Re-hash tasks that read dep outputs — those outputs are now on disk
578
763
  const tasksToRehash = batchResults
579
- .filter((r) => needsRehashAfterExecution.has(r.task.id) &&
764
+ .filter((r) => rehashAfterRun.has(r.task.id) &&
580
765
  (r.status === 'success' || r.status === 'failure'))
581
766
  .map((r) => r.task);
582
767
  if (tasksToRehash.length > 0) {
@@ -592,25 +777,6 @@ class TaskOrchestrator {
592
777
  if (batchResults.length > 0) {
593
778
  await this.postRunSteps(batchResults, doNotSkipCache, groupId);
594
779
  }
595
- // Update batch status based on all task results
596
- const hasFailures = taskEntries.some(([taskId]) => {
597
- const status = this.completedTasks.get(taskId);
598
- return status === 'failure' || status === 'skipped';
599
- });
600
- this.options.lifeCycle.setBatchStatus?.(batch.id, hasFailures ? "Failure" /* BatchStatus.Failure */ : "Success" /* BatchStatus.Success */);
601
- this.forkedProcessTaskRunner.cleanUpBatchProcesses();
602
- const tasksCompleted = taskEntries.filter(([taskId]) => this.completedTasks.has(taskId));
603
- // Batch is still not done, run it again
604
- if (tasksCompleted.length !== taskEntries.length) {
605
- await this.applyFromCacheOrRunBatch(doNotSkipCache, {
606
- id: batch.id,
607
- executorName: batch.executorName,
608
- taskGraph: (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, tasksCompleted.map(([taskId]) => taskId)),
609
- }, groupId);
610
- }
611
- // Batch is done, mark it as completed
612
- const applyFromCacheOrRunBatchEnd = perf_hooks_1.performance.mark('TaskOrchestrator-apply-from-cache-or-run-batch:end');
613
- perf_hooks_1.performance.measure('TaskOrchestrator-apply-from-cache-or-run-batch', applyFromCacheOrRunBatchStart.name, applyFromCacheOrRunBatchEnd.name);
614
780
  return [...cachedResults, ...batchResults];
615
781
  }
616
782
  async runBatch(batch, env, groupId) {
@@ -639,7 +805,8 @@ class TaskOrchestrator {
639
805
  // When the batch is being folded, printing is deferred to batch end
640
806
  // (printGroupedBatchOutput), which always renders each task through the
641
807
  // life cycle and adds the worker's whole log as a fold when the run
642
- // asked for full output or any task failed or was stopped.
808
+ // asked for full output or any task failed, was stopped or was never
809
+ // reached.
643
810
  if (status !== 'skipped' && !(0, output_1.shouldGroupBatchOutput)()) {
644
811
  this.options.lifeCycle.printTaskTerminalOutput(task, status, result.terminalOutput ?? '');
645
812
  }
@@ -734,7 +901,7 @@ class TaskOrchestrator {
734
901
  if (!capturedOutputPath || (0, output_1.printsTaskOutput)(this.resolvedOutputStyle)) {
735
902
  return;
736
903
  }
737
- const failed = results.some((r) => r.status === 'failure' || r.status === 'stopped');
904
+ const failed = results.some((r) => batchWorkerLogExplains(r.status));
738
905
  if (!failed) {
739
906
  return;
740
907
  }
@@ -784,8 +951,9 @@ class TaskOrchestrator {
784
951
  * those per-project summaries to the worker's stdio at all.
785
952
  *
786
953
  * The worker's whole captured log is rendered as a fold above them when the
787
- * run asked for full output, or when any task failed or was stopped. A
788
- * diagnostic that explains a failure is routinely one no task claimed:
954
+ * run asked for full output, or when any task failed, was stopped or was
955
+ * never reached.
956
+ * A diagnostic that explains a failure is routinely one no task claimed:
789
957
  * `@nx/maven`'s batch impl writes its exit-code dump and failed-task outputs
790
958
  * to the worker's stderr via `console.error`, and the Maven JVM it spawns
791
959
  * points slf4j at `System.out` because its own stderr carries the result
@@ -821,7 +989,7 @@ class TaskOrchestrator {
821
989
  verbose: this.options.verbose,
822
990
  outputStyle: this.resolvedOutputStyle,
823
991
  });
824
- const batchOwnsTheDiagnostic = taskResults.some((r) => r.status === 'failure' || r.status === 'stopped');
992
+ const batchOwnsTheDiagnostic = taskResults.some((r) => batchWorkerLogExplains(r.status));
825
993
  if ((0, output_1.printsTaskOutput)(this.resolvedOutputStyle) &&
826
994
  (printsFullOutput || batchOwnsTheDiagnostic) &&
827
995
  capturedOutputPath) {
@@ -855,7 +1023,9 @@ class TaskOrchestrator {
855
1023
  ? 'failure'
856
1024
  : taskResults.some((r) => r.status === 'stopped')
857
1025
  ? 'stopped'
858
- : 'success';
1026
+ : taskResults.some((r) => r.status === 'skipped')
1027
+ ? 'skipped'
1028
+ : 'success';
859
1029
  output_1.output.logBatchGroup(label, body, worst);
860
1030
  if (!redirectLines) {
861
1031
  return;
@@ -876,8 +1046,8 @@ class TaskOrchestrator {
876
1046
  * for running those via {@link runTaskDirectly}.
877
1047
  *
878
1048
  * Fires scheduleTask lifecycle for hits that haven't been through
879
- * processAllScheduledTasks yet. That's a coordinator gap-filler and
880
- * a no-op for callers that pre-process the schedule.
1049
+ * processTasks yet. That's a coordinator gap-filler and a no-op for
1050
+ * callers that process their tasks first.
881
1051
  *
882
1052
  * The caller provides `groupId` — cache hits share one slot since they
883
1053
  * don't actually compete for parallelism.
@@ -896,7 +1066,7 @@ class TaskOrchestrator {
896
1066
  return [];
897
1067
  // scheduleTask lifecycle for hits the coordinator resolved before
898
1068
  // processAllScheduledTasks could fire it. No-op for callers that
899
- // already ran processAllScheduledTasks (every hit is in processedTasks).
1069
+ // already ran processTasks (every hit is in processedTasks).
900
1070
  await Promise.all(cacheHits
901
1071
  .filter(({ task }) => !this.processedTasks.has(task.id))
902
1072
  .map(({ task }) => this.options.lifeCycle.scheduleTask(task)));
@@ -923,8 +1093,7 @@ class TaskOrchestrator {
923
1093
  // Wake coordinator — the delete above may satisfy the exit condition
924
1094
  // (pendingDiscreteWorkers.size === 0) that was missed when
925
1095
  // scheduleNextTasksAndReleaseThreads fired earlier.
926
- this.waitingForTasks.forEach((f) => f(null));
927
- this.waitingForTasks.length = 0;
1096
+ this.releaseThreads();
928
1097
  });
929
1098
  this.pendingDiscreteWorkers.add(worker);
930
1099
  }
@@ -947,13 +1116,36 @@ class TaskOrchestrator {
947
1116
  await this.postRunSteps([{ task, status: 'failure', terminalOutput }], doNotSkipCache, groupId);
948
1117
  }
949
1118
  /**
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).
1119
+ * Spawn and wait on a task's child process once its ready dependencies are
1120
+ * ready, with no cache lookup. Callers must have already confirmed the task
1121
+ * is a cache miss (or disabled caching entirely).
953
1122
  */
954
1123
  async runTaskDirectly(doNotSkipCache, task, groupId) {
955
1124
  // Wait for task to be processed
956
1125
  const taskSpecificEnv = await this.processedTasks.get(task.id);
1126
+ const waitError = await this.waitForReadyDependencies(task).then(() => null, (e) => e);
1127
+ // Skipped by a failed dependency's propagation while waiting
1128
+ if (this.completedTasks.has(task.id)) {
1129
+ return {
1130
+ task,
1131
+ code: 1,
1132
+ status: this.completedTasks.get(task.id),
1133
+ terminalOutput: '',
1134
+ };
1135
+ }
1136
+ if (waitError instanceof ready_when_1.ProducerFailedError) {
1137
+ await this.skipWaiter(task, groupId);
1138
+ return { task, code: 1, status: 'skipped', terminalOutput: '' };
1139
+ }
1140
+ if (waitError) {
1141
+ await this.failTaskBeforeStart(task, groupId, waitError, doNotSkipCache);
1142
+ return {
1143
+ task,
1144
+ code: 1,
1145
+ status: 'failure',
1146
+ terminalOutput: waitError.message,
1147
+ };
1148
+ }
957
1149
  await this.preRunSteps([task], { groupId });
958
1150
  const pipeOutput = await this.pipeOutputCapture(task);
959
1151
  const temporaryOutputPath = this.cache.temporaryOutputPath(task);
@@ -968,6 +1160,7 @@ class TaskOrchestrator {
968
1160
  const env = pipeOutput
969
1161
  ? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
970
1162
  : (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
1163
+ Object.assign(env, this.getReadyUrlEnv(task));
971
1164
  let resolveDiscreteExit;
972
1165
  const discreteExitHandled = new Promise((r) => (resolveDiscreteExit = r));
973
1166
  this.discreteTaskExitHandled.set(task.id, discreteExitHandled);
@@ -1005,27 +1198,21 @@ class TaskOrchestrator {
1005
1198
  try {
1006
1199
  const { schema } = (0, utils_1.getExecutorForTask)(task, this.projects);
1007
1200
  const combinedOptions = (0, params_1.combineOptionsForExecutor)(task.overrides, task.target.configuration ?? targetConfiguration.defaultConfiguration, targetConfiguration, schema, task.target.project, (0, path_1.relative)(task.projectRoot ?? workspace_root_1.workspaceRoot, process.cwd()), process.env.NX_VERBOSE_LOGGING === 'true');
1008
- if (combinedOptions.env) {
1009
- env = {
1010
- ...env,
1011
- ...combinedOptions.env,
1012
- };
1013
- }
1014
1201
  if (streamOutput) {
1015
1202
  const args = (0, utils_1.getPrintableCommandArgsForTask)(task);
1016
1203
  output_1.output.logCommand(args.join(' '));
1017
1204
  }
1018
1205
  const runCommandsOptions = {
1019
1206
  ...combinedOptions,
1020
- env,
1021
1207
  usePty: this.tuiEnabled ||
1022
1208
  (!this.tasksSchedule.hasTasks() &&
1023
1209
  this.runningContinuousTasks.size === 0),
1024
1210
  streamOutput: streamOutput && !shouldPrefix,
1025
1211
  };
1212
+ // As in a forked run, run-commands builds its env on the task env
1026
1213
  const runningTask = await (0, run_commands_impl_1.runCommands)(runCommandsOptions, {
1027
1214
  root: workspace_root_1.workspaceRoot, // only root is needed in runCommands
1028
- }, task);
1215
+ }, task, env);
1029
1216
  this.runningRunCommandsTasks.set(task.id, runningTask);
1030
1217
  runningTask.onExit(() => {
1031
1218
  this.runningRunCommandsTasks.delete(task.id);
@@ -1165,11 +1352,37 @@ class TaskOrchestrator {
1165
1352
  async startContinuousTask(task, groupId) {
1166
1353
  if (this.runningTasksService &&
1167
1354
  this.runningTasksService.getRunningTasks([task.id]).length) {
1355
+ // This branch never reads the hash, so a failure to compute it is dropped
1356
+ void this.processedTasks.get(task.id)?.catch(() => { });
1357
+ let readyWhen;
1358
+ let uncheckedReason;
1359
+ try {
1360
+ ({ readyWhen, uncheckedReason } = this.getReadinessCheck(task, () => this.getReadyWhenWait(task)));
1361
+ }
1362
+ catch (e) {
1363
+ return this.failTaskBeforeStart(task, groupId, e);
1364
+ }
1168
1365
  await this.preRunSteps([task], { groupId });
1169
1366
  if (this.tuiEnabled) {
1170
1367
  this.options.lifeCycle.setTaskStatus(task.id, 8 /* NativeTaskStatus.Shared */);
1171
1368
  }
1172
1369
  const runningTask = new shared_running_task_1.SharedRunningTask(this.runningTasksService, task.id);
1370
+ if (uncheckedReason) {
1371
+ this.warnUncheckedReadiness(task, uncheckedReason);
1372
+ }
1373
+ if (readyWhen) {
1374
+ const state = this.armReadiness(task.id);
1375
+ this.pollReadinessRow(task, readyWhen, {
1376
+ signal: state.abort.signal,
1377
+ ownerObserved: true,
1378
+ }).then(() => {
1379
+ if (!state.abort.signal.aborted)
1380
+ state.resolve();
1381
+ }, (e) => {
1382
+ if (!state.abort.signal.aborted)
1383
+ this.failReadiness(task, state, e);
1384
+ });
1385
+ }
1173
1386
  this.runningContinuousTasks.set(task.id, {
1174
1387
  runningTask,
1175
1388
  groupId,
@@ -1183,34 +1396,95 @@ class TaskOrchestrator {
1183
1396
  }));
1184
1397
  // task is already running by another process, we schedule the next tasks
1185
1398
  // and release the threads
1399
+ this.tasksSchedule.markContinuousTaskStarted(task.id);
1186
1400
  await this.scheduleNextTasksAndReleaseThreads();
1187
1401
  return runningTask;
1188
1402
  }
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);
1403
+ // Claim the task before doing any of the work below. getRunningTasks()
1404
+ // above is a check-then-act: until this row exists, a sibling Nx process
1405
+ // asking the same question is told the task is not running and starts a
1406
+ // duplicate of it.
1208
1407
  this.runningTasksService?.addRunningTask(task.id);
1209
- this.runningContinuousTasks.set(task.id, {
1210
- runningTask: childProcess,
1211
- groupId,
1212
- ownsRunningTasksService: true,
1213
- });
1408
+ let pipeOutput;
1409
+ let temporaryOutputPath;
1410
+ let streamOutput;
1411
+ let env;
1412
+ let readyWhen;
1413
+ let uncheckedReason;
1414
+ let readyOnStart;
1415
+ try {
1416
+ const taskSpecificEnv = await this.processedTasks.get(task.id);
1417
+ pipeOutput = await this.pipeOutputCapture(task);
1418
+ // obtain metadata
1419
+ temporaryOutputPath = this.cache.temporaryOutputPath(task);
1420
+ // Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
1421
+ // `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
1422
+ // only reports at `endCommand`, which never runs for a task that does not
1423
+ // end - so suppressing here would leave a `nx serve` under `summary` with a
1424
+ // permanently silent terminal and no file to read yet. Streaming is the only
1425
+ // channel a continuous task has.
1426
+ streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
1427
+ ? false
1428
+ : (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
1429
+ env = pipeOutput
1430
+ ? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
1431
+ : (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
1432
+ const readyWhenInputs = (0, ready_when_1.getReadyWhenInputs)(task, this.projectGraph, this.projects, workspace_root_1.workspaceRoot);
1433
+ this.checkReadyWhenBeforeWait(task, env, readyWhenInputs);
1434
+ await this.waitForReadyDependencies(task);
1435
+ Object.assign(env, this.getReadyUrlEnv(task));
1436
+ ({ readyWhen, uncheckedReason, readyOnStart } = this.getReadinessCheck(task, () => this.getReadyWhen(task, env, readyWhenInputs)));
1437
+ if (readyWhen?.kind === 'signal') {
1438
+ this.readySignal ??= new ready_signal_server_1.ReadySignalServer();
1439
+ Object.assign(env, await this.readySignal.env(task.id));
1440
+ }
1441
+ }
1442
+ catch (e) {
1443
+ this.runningTasksService?.removeRunningTask(task.id);
1444
+ if (e instanceof ready_when_1.ProducerFailedError) {
1445
+ await this.skipWaiter(task, groupId);
1446
+ return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: '' });
1447
+ }
1448
+ return this.failTaskBeforeStart(task, groupId, e);
1449
+ }
1450
+ // Skipped by a failed dependency's propagation while waiting
1451
+ if (this.completedTasks.has(task.id)) {
1452
+ this.runningTasksService?.removeRunningTask(task.id);
1453
+ return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: '' });
1454
+ }
1455
+ let childProcess;
1456
+ try {
1457
+ await this.preRunSteps([task], { groupId });
1458
+ this.detectTaskInvocationLoop(task);
1459
+ childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
1460
+ this.tasksSchedule.markContinuousTaskStarted(task.id);
1461
+ if (uncheckedReason) {
1462
+ this.warnUncheckedReadiness(task, uncheckedReason);
1463
+ // No probe settles the row, and a waiter in another process polls it
1464
+ this.publishReadiness(task, 2 /* TaskReadiness.Failed */);
1465
+ }
1466
+ if (readyWhen) {
1467
+ this.startReadinessProbe(task, readyWhen, childProcess);
1468
+ }
1469
+ // Waiters here, in other processes and in the TUI hold on a configured
1470
+ // readyWhen until it gets a verdict
1471
+ if (readyOnStart) {
1472
+ this.recordReadiness(task, 1 /* TaskReadiness.Ready */);
1473
+ this.armReadiness(task.id).resolve();
1474
+ }
1475
+ this.runningContinuousTasks.set(task.id, {
1476
+ runningTask: childProcess,
1477
+ groupId,
1478
+ ownsRunningTasksService: true,
1479
+ });
1480
+ }
1481
+ catch (e) {
1482
+ // Nothing owns the claim until runningContinuousTasks holds it — the
1483
+ // release paths (completeContinuousTask, the signal handler) both iterate
1484
+ // that map. Drop it here or siblings wait on a task no one is running.
1485
+ this.runningTasksService?.removeRunningTask(task.id);
1486
+ throw e;
1487
+ }
1214
1488
  this.continuousTaskExitHandled.set(task.id, new Promise((resolve) => {
1215
1489
  childProcess.onExit(async (code) => {
1216
1490
  await this.handleContinuousTaskExit(code, task, groupId, true);
@@ -1224,6 +1498,299 @@ class TaskOrchestrator {
1224
1498
  return childProcess;
1225
1499
  }
1226
1500
  // endregion Single Task
1501
+ // region Readiness
1502
+ getReadyWhen(task, env, inputs) {
1503
+ return (0, ready_when_1.getReadyWhen)(task, this.projectGraph, this.projects, workspace_root_1.workspaceRoot, env, inputs);
1504
+ }
1505
+ // NX_READY_URL is known only after the wait, so fail now only when the
1506
+ // check fails without it and, if a producer may report one, with a URL too
1507
+ checkReadyWhenBeforeWait(task, env, inputs) {
1508
+ const producerIds = (0, ready_when_1.getReadyProducerIds)(task, this.fullTaskGraph);
1509
+ if (!inputs || !producerIds.length) {
1510
+ return;
1511
+ }
1512
+ const envs = [env];
1513
+ if (producerIds.some((id) => (0, ready_when_1.mayReportReadyUrl)(this.fullTaskGraph.tasks[id], this.projectGraph))) {
1514
+ envs.push({ ...env, [ready_signal_1.READY_URL_ENV]: 'http://localhost/' });
1515
+ }
1516
+ const errors = envs.map((checkEnv) => {
1517
+ try {
1518
+ this.getReadyWhen(task, checkEnv, inputs);
1519
+ return null;
1520
+ }
1521
+ catch (e) {
1522
+ return e;
1523
+ }
1524
+ });
1525
+ if (errors.every(Boolean)) {
1526
+ this.getReadinessCheck(task, () => {
1527
+ throw errors[errors.length - 1];
1528
+ });
1529
+ }
1530
+ }
1531
+ // Only the process that runs a producer knows the env its probe reads
1532
+ getReadyWhenWait(task) {
1533
+ return (0, ready_when_1.getReadyWhenWait)(task, this.projectGraph, this.projects, workspace_root_1.workspaceRoot);
1534
+ }
1535
+ // An invalid readyWhen fails the task before it starts only when something
1536
+ // in this run waits for it; otherwise the task runs without a check
1537
+ getReadinessCheck(task, resolve) {
1538
+ try {
1539
+ const readyWhen = resolve();
1540
+ return {
1541
+ readyWhen,
1542
+ // A readyWhen resolves to no check when the server picks its port
1543
+ readyOnStart: readyWhen === null &&
1544
+ (0, ready_when_1.getReadyWhenConfig)(task, this.projectGraph) != null,
1545
+ };
1546
+ }
1547
+ catch (e) {
1548
+ if (!(e instanceof ready_when_1.InvalidReadyWhenError) ||
1549
+ this.hasReadyWaiter(task.id)) {
1550
+ throw e;
1551
+ }
1552
+ return { readyWhen: null, uncheckedReason: e.message };
1553
+ }
1554
+ }
1555
+ // Call it after the start: the TUI replaces the task's terminal when it
1556
+ // starts, dropping output appended before
1557
+ warnUncheckedReadiness(task, reason) {
1558
+ const message = `${reason} Nothing in this run waits for it to be ready, so it runs without a readiness check.`;
1559
+ if (this.tuiEnabled) {
1560
+ this.options.lifeCycle.appendTaskOutput(task.id, `\r\n${message}\r\n`, false);
1561
+ }
1562
+ else {
1563
+ console.warn(message);
1564
+ }
1565
+ }
1566
+ async waitForReadyDependencies(task) {
1567
+ const producerIds = (0, ready_when_1.getReadyProducerIds)(task, this.fullTaskGraph);
1568
+ for (const producerId of producerIds) {
1569
+ const producer = this.fullTaskGraph.tasks[producerId];
1570
+ let readyWhen;
1571
+ try {
1572
+ readyWhen = this.getReadyWhenWait(producer);
1573
+ }
1574
+ catch (e) {
1575
+ // An owner in another process runs it unchecked and marks its row
1576
+ // failed, or drops the row. Without a row, nothing will settle it.
1577
+ if (!(e instanceof ready_when_1.InvalidReadyWhenError) ||
1578
+ this.taskGraph.tasks[producerId]) {
1579
+ throw e;
1580
+ }
1581
+ const owned = await this.pollReadinessRow(producer, { timeout: ready_when_1.DEFAULT_READY_TIMEOUT, description: 'invalid' }, { onPending: () => this.onReadinessHold(producerId) });
1582
+ if (!owned) {
1583
+ throw e;
1584
+ }
1585
+ continue;
1586
+ }
1587
+ if (!readyWhen) {
1588
+ if (process.env.NX_VERBOSE_LOGGING === 'true') {
1589
+ console.log(`Task "${producer.id}" declares no "readyWhen", so "${task.id}" runs once it has started.`);
1590
+ }
1591
+ continue;
1592
+ }
1593
+ // A producer in this task graph is held by dispatch and settled by its
1594
+ // probe or row poll; one outside it (an agent's flat graph) has only its row
1595
+ if (this.taskGraph.tasks[producerId]) {
1596
+ await this.readinessOf(producerId).promise;
1597
+ }
1598
+ else {
1599
+ await this.pollReadinessRow(producer, readyWhen, {
1600
+ onPending: () => this.onReadinessHold(producerId),
1601
+ });
1602
+ }
1603
+ }
1604
+ }
1605
+ // A waiter learns the url of the one ready producer that reported it
1606
+ getReadyUrlEnv(task) {
1607
+ const urls = (0, ready_when_1.getReadyProducerIds)(task, this.fullTaskGraph)
1608
+ .map((id) => this.readyUrls.get(id))
1609
+ .filter(Boolean);
1610
+ return urls.length === 1 ? { [ready_signal_1.READY_URL_ENV]: urls[0] } : {};
1611
+ }
1612
+ onReadinessHold(producerId) {
1613
+ if (!this.waitingLogged.has(producerId) &&
1614
+ !this.tuiEnabled &&
1615
+ (0, output_1.printsTaskOutput)(this.resolvedOutputStyle)) {
1616
+ this.waitingLogged.add(producerId);
1617
+ console.log(`Waiting for "${producerId}" to be ready...`);
1618
+ }
1619
+ }
1620
+ // A row absent from the start means no Nx process this one can see owns the
1621
+ // producer (resolves false), unless the caller already saw the owner: then it
1622
+ // exited. A row that disappears mid-wait means the producer exited.
1623
+ async pollReadinessRow(producer, readyWhen, { signal, ownerObserved = false, onPending, } = {}) {
1624
+ const deadline = Date.now() + readyWhen.timeout;
1625
+ let lastStatus = null;
1626
+ while (true) {
1627
+ if (signal?.aborted) {
1628
+ throw (0, ready_when_1.notReadyError)(producer.id, 'exited');
1629
+ }
1630
+ if (this.stopRequested || this.bailed) {
1631
+ throw (0, ready_when_1.notReadyError)(producer.id, 'was stopped');
1632
+ }
1633
+ const status = this.runningTasksService?.getTaskReadiness(producer.id) ?? null;
1634
+ if (status === null) {
1635
+ if (lastStatus !== null || ownerObserved) {
1636
+ throw (0, ready_when_1.notReadyError)(producer.id, 'exited');
1637
+ }
1638
+ return false;
1639
+ }
1640
+ if (status !== lastStatus) {
1641
+ lastStatus = status;
1642
+ this.options.lifeCycle.setTaskReadiness?.(producer.id, status);
1643
+ }
1644
+ if (status === 1 /* TaskReadiness.Ready */) {
1645
+ this.readReadyUrl(producer.id);
1646
+ return true;
1647
+ }
1648
+ if (status === 2 /* TaskReadiness.Failed */) {
1649
+ throw new ready_when_1.ProducerFailedError(producer.id);
1650
+ }
1651
+ if (Date.now() >= deadline) {
1652
+ throw (0, ready_when_1.readinessTimeoutError)(producer.id, readyWhen);
1653
+ }
1654
+ onPending?.();
1655
+ await new Promise((resolve) => setTimeout(resolve, 100));
1656
+ }
1657
+ }
1658
+ startReadinessProbe(task, readyWhen, runningTask) {
1659
+ const state = this.armReadiness(task.id);
1660
+ this.options.lifeCycle.setTaskReadiness?.(task.id, 0 /* TaskReadiness.Pending */);
1661
+ (0, probes_1.waitForReadiness)(readyWhen, {
1662
+ taskId: task.id,
1663
+ runningTask,
1664
+ cwd: workspace_root_1.workspaceRoot,
1665
+ signal: state.abort.signal,
1666
+ readySignal: this.readySignal,
1667
+ }).then(({ url }) => {
1668
+ if (state.abort.signal.aborted)
1669
+ return;
1670
+ if (url)
1671
+ this.readyUrls.set(task.id, url);
1672
+ this.recordReadiness(task, 1 /* TaskReadiness.Ready */, url);
1673
+ state.resolve();
1674
+ }, (e) => {
1675
+ if (state.abort.signal.aborted)
1676
+ return;
1677
+ this.recordReadiness(task, 2 /* TaskReadiness.Failed */);
1678
+ this.failReadiness(task, state, e);
1679
+ });
1680
+ }
1681
+ // A verdict against a producer something in this run waits for fails the
1682
+ // producer, and the skip propagation covers the waiters. Without a waiter
1683
+ // the producer is left alone, as a plain `nx serve` with a probe would be.
1684
+ async failReadiness(task, state, e) {
1685
+ const message = e?.message ?? String(e);
1686
+ const entry = this.runningContinuousTasks.get(task.id);
1687
+ if (entry &&
1688
+ !(e instanceof ready_when_1.NotReadyError) &&
1689
+ this.hasReadyWaiter(task.id)) {
1690
+ if (this.tuiEnabled) {
1691
+ this.options.lifeCycle.appendTaskOutput(task.id, `\r\n${message}\r\n`, false);
1692
+ }
1693
+ else {
1694
+ console.error(message);
1695
+ }
1696
+ this.readinessFailureReasons.set(task.id, message);
1697
+ const completion = this.complete([{ task, status: 'failure', terminalOutput: message }], entry.groupId);
1698
+ this.readinessFailures.set(task.id, completion);
1699
+ try {
1700
+ await completion;
1701
+ }
1702
+ finally {
1703
+ this.readinessFailures.delete(task.id);
1704
+ }
1705
+ }
1706
+ else if (process.env.NX_VERBOSE_LOGGING === 'true') {
1707
+ console.error(message);
1708
+ }
1709
+ state.reject(e);
1710
+ }
1711
+ // On an agent the waiter runs in another process with its own flat graph
1712
+ hasReadyWaiter(producerId) {
1713
+ return Object.values(this.fullTaskGraph.tasks).some((task) => (0, ready_when_1.getReadyProducerIds)(task, this.fullTaskGraph).includes(producerId));
1714
+ }
1715
+ // The producer is not in this task graph, so no skip propagates to the waiter
1716
+ async skipWaiter(task, groupId) {
1717
+ if (!this.completedTasks.has(task.id)) {
1718
+ await this.complete([{ task, status: 'skipped' }], groupId);
1719
+ }
1720
+ }
1721
+ recordReadiness(task, status, url) {
1722
+ this.options.lifeCycle.setTaskReadiness?.(task.id, status);
1723
+ this.publishReadiness(task, status, url);
1724
+ }
1725
+ // Only other processes read the row, so a failed write must not fail this one
1726
+ publishReadiness(task, status, url) {
1727
+ try {
1728
+ this.runningTasksService?.setTaskReadiness(task.id, status, url ?? null);
1729
+ }
1730
+ catch (e) {
1731
+ if (process.env.NX_VERBOSE_LOGGING === 'true') {
1732
+ console.error(`Failed to record readiness of "${task.id}":`, e);
1733
+ }
1734
+ }
1735
+ }
1736
+ // The url a producer another process runs reported with its readiness
1737
+ readReadyUrl(producerId) {
1738
+ try {
1739
+ const url = this.runningTasksService?.getTaskReadyUrl(producerId);
1740
+ if (url)
1741
+ this.readyUrls.set(producerId, url);
1742
+ }
1743
+ catch (e) {
1744
+ if (process.env.NX_VERBOSE_LOGGING === 'true') {
1745
+ console.error(`Failed to read the url of "${producerId}":`, e);
1746
+ }
1747
+ }
1748
+ }
1749
+ readinessOf(taskId) {
1750
+ return this.readiness.get(taskId) ?? this.armReadiness(taskId);
1751
+ }
1752
+ // Replaces a state settled by an earlier run so a restart re-arms it
1753
+ armReadiness(taskId) {
1754
+ let state = this.readiness.get(taskId);
1755
+ if (!state || state.settled) {
1756
+ this.tasksSchedule.markReadinessPending(taskId);
1757
+ state = createReadinessState((outcome) => {
1758
+ if (outcome === 'ready') {
1759
+ this.tasksSchedule.markReady(taskId);
1760
+ }
1761
+ else {
1762
+ this.tasksSchedule.markReadinessFailed(taskId);
1763
+ }
1764
+ this.readinessSettled = true;
1765
+ this.releaseThreads();
1766
+ });
1767
+ this.readiness.set(taskId, state);
1768
+ }
1769
+ return state;
1770
+ }
1771
+ abortReadiness(taskId, reason) {
1772
+ const state = this.readiness.get(taskId);
1773
+ if (!state)
1774
+ return;
1775
+ state.abort.abort();
1776
+ state.reject((0, ready_when_1.notReadyError)(taskId, reason));
1777
+ }
1778
+ abortAllReadiness() {
1779
+ for (const taskId of this.readiness.keys()) {
1780
+ this.abortReadiness(taskId, 'was stopped');
1781
+ }
1782
+ }
1783
+ async failTaskBeforeStart(task, groupId, e, doNotSkipCache = false) {
1784
+ this.abortReadiness(task.id, 'failed');
1785
+ // Already skipped by a failed dependency's propagation
1786
+ if (!this.completedTasks.has(task.id)) {
1787
+ // Lifecycles pair endTasks with startTasks, so start it before failing it
1788
+ await this.preRunSteps([task], { groupId });
1789
+ await this.handleDiscreteWorkerFailure(doNotSkipCache, task, groupId, e);
1790
+ }
1791
+ return new noop_child_process_1.NoopChildProcess({ code: 1, terminalOutput: e?.message ?? '' });
1792
+ }
1793
+ // endregion Readiness
1227
1794
  // region Lifecycle
1228
1795
  async preRunSteps(tasks, metadata) {
1229
1796
  const now = Date.now();
@@ -1364,7 +1931,13 @@ class TaskOrchestrator {
1364
1931
  if (!task.hash)
1365
1932
  return;
1366
1933
  try {
1367
- const { size } = (0, fs_1.statSync)(this.cache.temporaryOutputPath(task));
1934
+ const outputPath = this.cache.temporaryOutputPath(task);
1935
+ // The runner's exit handler wrote the child's transcript alone
1936
+ const reason = this.readinessFailureReasons.get(task.id);
1937
+ if (reason) {
1938
+ (0, fs_1.appendFileSync)(outputPath, `\n${reason}\n`);
1939
+ }
1940
+ const { size } = (0, fs_1.statSync)(outputPath);
1368
1941
  this.cache.recordTerminalOutputs([{ hash: task.hash, size }]);
1369
1942
  }
1370
1943
  catch {
@@ -1373,12 +1946,13 @@ class TaskOrchestrator {
1373
1946
  }
1374
1947
  async scheduleNextTasksAndReleaseThreads() {
1375
1948
  if (this.stopRequested) {
1376
- this.waitingForTasks.forEach((f) => f(null));
1377
- this.waitingForTasks.length = 0;
1949
+ this.releaseThreads();
1378
1950
  return;
1379
1951
  }
1380
1952
  await this.tasksSchedule.scheduleNextTasks();
1381
- // release blocked threads
1953
+ this.releaseThreads();
1954
+ }
1955
+ releaseThreads() {
1382
1956
  this.waitingForTasks.forEach((f) => f(null));
1383
1957
  this.waitingForTasks.length = 0;
1384
1958
  }
@@ -1425,8 +1999,7 @@ class TaskOrchestrator {
1425
1999
  if (this.completedTasks.has(task.id))
1426
2000
  continue;
1427
2001
  this.completedTasks.set(task.id, status);
1428
- this.taskInvocationTracker?.unregisterTask(task.id);
1429
- this.registeredInvocations.delete(task.id);
2002
+ this.releaseTaskInvocation(task);
1430
2003
  if (this.tuiEnabled) {
1431
2004
  this.options.lifeCycle.setTaskStatus(task.id, displayStatus ?? (0, native_1.parseTaskStatus)(status));
1432
2005
  }
@@ -1437,6 +2010,7 @@ class TaskOrchestrator {
1437
2010
  // mark the execution as bailed which will stop all further execution
1438
2011
  // only the tasks that are currently running will finish
1439
2012
  this.bailed = true;
2013
+ this.abortAllReadiness();
1440
2014
  }
1441
2015
  else {
1442
2016
  // Collect reverse deps to skip
@@ -1467,6 +2041,14 @@ class TaskOrchestrator {
1467
2041
  if (process.env.NX_NATIVE_COMMAND_RUNNER !== 'false') {
1468
2042
  return true;
1469
2043
  }
2044
+ // The log probe reads the captured output. Read from the config, because
2045
+ // the probe resolves against the env that depends on this
2046
+ const readyWhen = (0, ready_when_1.getReadyWhenConfig)(task, this.projectGraph);
2047
+ if (typeof readyWhen === 'object' &&
2048
+ readyWhen &&
2049
+ 'logMatches' in readyWhen) {
2050
+ return true;
2051
+ }
1470
2052
  // When TUI is enabled, we need to use pipe output capture to support
1471
2053
  // progressive output streaming via the onOutput callback
1472
2054
  if (this.tuiEnabled) {
@@ -1520,8 +2102,13 @@ class TaskOrchestrator {
1520
2102
  }
1521
2103
  // endregion utils
1522
2104
  async handleContinuousTaskExit(code, task, groupId, ownsRunningTasksService) {
1523
- // If cleanup already completed this task, nothing left to do
2105
+ await this.readinessFailures.get(task.id);
2106
+ // Completed by cleanup, or failed its readiness check while still running
1524
2107
  if (this.completedTasks.has(task.id)) {
2108
+ if (this.runningContinuousTasks.delete(task.id) &&
2109
+ ownsRunningTasksService) {
2110
+ this.runningTasksService?.removeRunningTask(task.id);
2111
+ }
1525
2112
  return;
1526
2113
  }
1527
2114
  const stoppingReason = this.runningContinuousTasks.get(task.id)?.stoppingReason;
@@ -1542,15 +2129,20 @@ class TaskOrchestrator {
1542
2129
  isContinuousTaskNeeded(taskId) {
1543
2130
  return this.tasksSchedule
1544
2131
  .getIncompleteTasks()
1545
- .some((t) => this.taskGraph.continuousDependencies[t.id]?.includes(taskId));
2132
+ .some((t) => this.taskGraph.continuousDependencies[t.id]?.some((edge) => edge.id === taskId));
1546
2133
  }
1547
2134
  async completeContinuousTask(task, groupId, ownsRunningTasksService, reason) {
1548
- if (this.completedTasks.has(task.id))
1549
- return;
2135
+ await this.readinessFailures.get(task.id);
1550
2136
  this.runningContinuousTasks.delete(task.id);
1551
2137
  if (ownsRunningTasksService) {
1552
2138
  this.runningTasksService?.removeRunningTask(task.id);
1553
2139
  }
2140
+ // A probe that does not need the child (url, port, command) could still
2141
+ // pass during completion and release dependents of an exited producer
2142
+ this.readiness.get(task.id)?.abort.abort();
2143
+ // Failed its readiness check while still running
2144
+ if (this.completedTasks.has(task.id))
2145
+ return;
1554
2146
  task.endTime = Date.now();
1555
2147
  if (reason === 'fulfilled') {
1556
2148
  await this.complete([
@@ -1567,6 +2159,11 @@ class TaskOrchestrator {
1567
2159
  else {
1568
2160
  await this.complete([{ task, status: 'stopped' }], groupId);
1569
2161
  }
2162
+ // After the skip propagation above, so a waiter is reported as skipped
2163
+ // rather than as failed
2164
+ this.abortReadiness(task.id, 'exited');
2165
+ // Skipped dependents leave nothing in flight to wake the loops
2166
+ this.releaseThreads();
1570
2167
  }
1571
2168
  async cleanup() {
1572
2169
  if (this.cleanupPromise) {
@@ -1576,6 +2173,9 @@ class TaskOrchestrator {
1576
2173
  return this.cleanupPromise;
1577
2174
  }
1578
2175
  async performCleanup() {
2176
+ // Waiters must not sit out a probe once the run is stopping
2177
+ this.abortAllReadiness();
2178
+ this.readySignal?.close();
1579
2179
  // Mark all running tasks for intentional stop
1580
2180
  const reason = this.stopRequested ? 'interrupted' : 'fulfilled';
1581
2181
  for (const entry of this.runningContinuousTasks.values()) {
@@ -1716,7 +2316,7 @@ class TaskOrchestrator {
1716
2316
  }
1717
2317
  const continuousDependencies = this.taskGraph.continuousDependencies[task.id];
1718
2318
  for (const continuousDependency of continuousDependencies) {
1719
- neededContinuousTasks.add(continuousDependency);
2319
+ neededContinuousTasks.add(continuousDependency.id);
1720
2320
  }
1721
2321
  }
1722
2322
  for (const [taskId, entry] of this.runningContinuousTasks) {