nx 24.0.0-beta.0 → 24.0.0-beta.2

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 (90) hide show
  1. package/dist/nx-cloud-internals.d.ts +1 -0
  2. package/dist/nx-cloud-internals.js +7 -1
  3. package/dist/src/command-line/graph/graph.js +2 -1
  4. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +1 -1
  5. package/dist/src/command-line/migrate/agentic/close-agent-session.js +4 -3
  6. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +19 -23
  7. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +16 -19
  8. package/dist/src/command-line/migrate/agentic/master/run-master-session.js +25 -13
  9. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +82 -22
  10. package/dist/src/command-line/migrate/agentic/select.js +3 -3
  11. package/dist/src/command-line/migrate/agentic/types.js +1 -3
  12. package/dist/src/command-line/migrate/command-object.js +5 -9
  13. package/dist/src/command-line/migrate/execute-migration.d.ts +2 -2
  14. package/dist/src/command-line/migrate/execute-migration.js +18 -10
  15. package/dist/src/command-line/migrate/migrate-analytics.d.ts +6 -0
  16. package/dist/src/command-line/migrate/migrate-analytics.js +12 -0
  17. package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -1
  18. package/dist/src/command-line/migrate/migrate-commits.js +10 -3
  19. package/dist/src/command-line/migrate/migrate-config.d.ts +6 -6
  20. package/dist/src/command-line/migrate/migrate-config.js +12 -6
  21. package/dist/src/command-line/migrate/migrate.d.ts +3 -30
  22. package/dist/src/command-line/migrate/migrate.js +206 -237
  23. package/dist/src/command-line/migrate/run/agent-output.d.ts +1 -0
  24. package/dist/src/command-line/migrate/run/agent-output.js +5 -0
  25. package/dist/src/command-line/migrate/run/broker.d.ts +14 -5
  26. package/dist/src/command-line/migrate/run/broker.js +68 -20
  27. package/dist/src/command-line/migrate/run/existing-run-report.d.ts +1 -1
  28. package/dist/src/command-line/migrate/run/existing-run-report.js +7 -5
  29. package/dist/src/command-line/migrate/run/index.d.ts +6 -5
  30. package/dist/src/command-line/migrate/run/index.js +12 -1
  31. package/dist/src/command-line/migrate/run/orchestrator.d.ts +21 -0
  32. package/dist/src/command-line/migrate/run/orchestrator.js +149 -32
  33. package/dist/src/command-line/migrate/run/run-state.d.ts +13 -0
  34. package/dist/src/command-line/migrate/run/run-state.js +21 -0
  35. package/dist/src/command-line/migrate/run/runbook.js +19 -0
  36. package/dist/src/command-line/migrate/run/state-machine.js +4 -1
  37. package/dist/src/command-line/migrate/run/util.d.ts +3 -0
  38. package/dist/src/command-line/migrate/run/util.js +7 -1
  39. package/dist/src/command-line/migrate/sort-migrations.d.ts +1 -7
  40. package/dist/src/command-line/migrate/sort-migrations.js +1 -12
  41. package/dist/src/command-line/show/show-target/info.js +1 -1
  42. package/dist/src/config/nx-json.d.ts +13 -4
  43. package/dist/src/config/task-graph.d.ts +1 -1
  44. package/dist/src/config/workspace-json-project-json.d.ts +72 -0
  45. package/dist/src/core/graph/main.js +1 -1
  46. package/dist/src/devkit-exports.d.ts +1 -1
  47. package/dist/src/hasher/check-task-files.js +4 -4
  48. package/dist/src/hasher/native-task-hasher-impl.js +3 -1
  49. package/dist/src/hasher/task-hasher.d.ts +2 -0
  50. package/dist/src/hasher/task-hasher.js +4 -1
  51. package/dist/src/native/index.d.ts +92 -14
  52. package/dist/src/native/native-bindings.js +3 -0
  53. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  54. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  55. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +6 -1
  56. package/dist/src/project-graph/utils/retrieve-workspace-files.js +27 -6
  57. package/dist/src/tasks-runner/create-task-graph.d.ts +7 -16
  58. package/dist/src/tasks-runner/create-task-graph.js +43 -57
  59. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  60. package/dist/src/tasks-runner/init-tasks-runner.js +13 -0
  61. package/dist/src/tasks-runner/life-cycle.d.ts +4 -1
  62. package/dist/src/tasks-runner/life-cycle.js +7 -0
  63. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +5 -2
  64. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +13 -7
  65. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +4 -3
  66. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +9 -2
  67. package/dist/src/tasks-runner/readiness/probes.d.ts +9 -0
  68. package/dist/src/tasks-runner/readiness/probes.js +62 -0
  69. package/dist/src/tasks-runner/readiness/ready-when.d.ts +38 -0
  70. package/dist/src/tasks-runner/readiness/ready-when.js +190 -0
  71. package/dist/src/tasks-runner/run-command.js +7 -6
  72. package/dist/src/tasks-runner/running-tasks/batch-process.js +6 -0
  73. package/dist/src/tasks-runner/running-tasks/node-child-process.js +5 -4
  74. package/dist/src/tasks-runner/task-graph-utils.d.ts +9 -6
  75. package/dist/src/tasks-runner/task-graph-utils.js +17 -12
  76. package/dist/src/tasks-runner/task-orchestrator.d.ts +37 -10
  77. package/dist/src/tasks-runner/task-orchestrator.js +459 -79
  78. package/dist/src/tasks-runner/tasks-runner.d.ts +1 -1
  79. package/dist/src/tasks-runner/tasks-schedule.d.ts +18 -1
  80. package/dist/src/tasks-runner/tasks-schedule.js +64 -6
  81. package/dist/src/tasks-runner/utils.js +6 -22
  82. package/dist/src/utils/child-process.js +1 -0
  83. package/dist/src/utils/git-utils.d.ts +6 -1
  84. package/dist/src/utils/git-utils.js +26 -5
  85. package/dist/src/utils/plugins/core-plugins.js +4 -0
  86. package/dist/src/utils/spawn-without-terminal.d.ts +27 -0
  87. package/dist/src/utils/spawn-without-terminal.js +110 -0
  88. package/package.json +12 -11
  89. package/schemas/nx-schema.json +93 -4
  90. package/schemas/project-schema.json +84 -2
@@ -5,6 +5,9 @@ exports.runOrchestratorResume = runOrchestratorResume;
5
5
  exports.holdRunToContinue = holdRunToContinue;
6
6
  exports.activeRunToReplace = activeRunToReplace;
7
7
  exports.releaseRunToHandOff = releaseRunToHandOff;
8
+ exports.activeRunForClassic = activeRunForClassic;
9
+ exports.checkRunForStartFresh = checkRunForStartFresh;
10
+ exports.deleteRunForStartFresh = deleteRunForStartFresh;
8
11
  exports.runOrchestratorReconcile = runOrchestratorReconcile;
9
12
  exports.completionWarnings = completionWarnings;
10
13
  const child_process_1 = require("child_process");
@@ -42,7 +45,7 @@ const issues_1 = require("./issues");
42
45
  const runbook_1 = require("./runbook");
43
46
  const existing_run_report_1 = require("./existing-run-report");
44
47
  const package_manager_1 = require("../../../utils/package-manager");
45
- // The dark migrate orchestrator: drives a durable run one dispense at a time.
48
+ // The migrate orchestrator: drives a durable run one dispense at a time.
46
49
  // An outer AI agent runs each dispensed command and re-invokes `nx migrate
47
50
  // --run-id=<id>` to reconcile; there is no long-lived process.
48
51
  const PLAN_SNAPSHOT_0 = 'plan-0.json';
@@ -55,7 +58,11 @@ const HANG_THRESHOLD_MS = 15 * 60 * 1000;
55
58
  const NO_PROGRESS_THRESHOLD = 3;
56
59
  // Two rearms per step: one for the diagnosed fix and one for its correction.
57
60
  const REARM_ESCALATION_CAP = 2;
58
- const INIT_CONTINUE_HINT = 're-run the command, or unset NX_MIGRATE_ORCHESTRATOR to use the standard migrate flow.';
61
+ const INIT_CONTINUE_HINT = 're-run the command, or re-run it with --agentic=false to use the standard migrate flow.';
62
+ const ID_PROBLEM_REFUSALS = {
63
+ 'not-shell-safe': 'contains characters that are not shell-safe. Orchestrated runs require shell-safe migration ids.',
64
+ duplicate: 'is listed more than once in the plan. Orchestrated runs require each migration id once.',
65
+ };
59
66
  function continueRunHint(runId) {
60
67
  return `re-run the command to continue run '${runId}'.`;
61
68
  }
@@ -92,9 +99,7 @@ function refuseUnsafeScratchExposure(exposure, thenWhat) {
92
99
  async function runOrchestratorInit(input) {
93
100
  const { root, migrationsJson, migrationsPath, createCommits, commitPrefix, skipInstall, installedNxVersion, validate, finalValidation, emitAgentInstructions = true, onExistingRun = 'report', replaceRunId, confirmStart, } = input;
94
101
  const migrations = (migrationsJson.migrations ?? []);
95
- const sorted = (0, sort_migrations_1.sortMigrations)(migrations.slice(), {
96
- hoistHandoffGitignore: true,
97
- });
102
+ const sorted = (0, sort_migrations_1.sortMigrations)(migrations.slice());
98
103
  const plannedIds = sorted.map((m) => `${m.package}:${m.name}`);
99
104
  // An active run means a prior init already happened (e.g. it crashed before
100
105
  // the agent's first reconcile); starting a second run would compete with it,
@@ -124,13 +129,13 @@ async function runOrchestratorInit(input) {
124
129
  }
125
130
  });
126
131
  }
127
- // Dispensed commands interpolate migration ids verbatim, so every init
128
- // validates the incoming plan's ids. Before the delete below: a run must
129
- // not be thrown away for a plan that cannot start.
130
- for (const id of plannedIds) {
131
- if (!run_state_1.SHELL_SAFE_VALUE.test(id)) {
132
- throw new Error(`The migration id '${id}' contains characters that are not shell-safe. Orchestrated runs require shell-safe migration ids.`);
133
- }
132
+ // Dispensed commands interpolate migration ids verbatim and a worker finds
133
+ // its step by id, so every init validates the incoming plan's ids. Before
134
+ // the delete below: a run must not be thrown away for a plan that cannot
135
+ // start.
136
+ const [idProblem] = (0, run_state_1.migrationIdProblems)(sorted);
137
+ if (idProblem !== undefined) {
138
+ throw new Error(`The migration id '${idProblem.id}' ${ID_PROBLEM_REFUSALS[idProblem.problem]} To run the plan without orchestration, re-run with --agentic=false.`);
134
139
  }
135
140
  if (confirmStart && !(await confirmStart())) {
136
141
  return { kind: 'refused' };
@@ -152,9 +157,8 @@ async function runOrchestratorInit(input) {
152
157
  // Applied before the checkpoint so the entry (when it can be added) already
153
158
  // covers older scratch by the time the checkpoint's `git add -A` runs; the
154
159
  // fallback's standalone commit is suppressed because that checkpoint
155
- // carries the edit. Unlike the classic loop, a planned ignore migration
156
- // can't be deferred to: the run dir is created below, before that
157
- // migration runs.
160
+ // carries the edit. A planned ignore migration can't be deferred to: the
161
+ // run dir is created below, before that migration runs.
158
162
  await (0, handoff_gitignore_1.applyAgenticHandoffGitignoreFallback)({
159
163
  migrations: sorted,
160
164
  installedNxVersion,
@@ -314,11 +318,65 @@ function activeRunToReplace(root, runId) {
314
318
  function releaseRunToHandOff(root, runId) {
315
319
  (0, state_lock_1.releaseRunActivity)((0, run_state_1.runDir)(root, runId));
316
320
  }
321
+ /**
322
+ * The active run a classic `--run-migrations` must not run over, held while
323
+ * the caller reports it or asks what to do; null when no run is active. Run
324
+ * directories that cannot be read refuse as they do for init.
325
+ */
326
+ function activeRunForClassic(root, plannedIds) {
327
+ const active = findActiveRunForInit(root);
328
+ if (!active)
329
+ return null;
330
+ // Held before the facts are read, as init's report does.
331
+ (0, state_lock_1.holdRunActivity)(root, active.runId);
332
+ return {
333
+ runId: active.runId,
334
+ facts: (0, existing_run_report_1.collectExistingRunFacts)(root, active.runId, active.state, plannedIds),
335
+ };
336
+ }
337
+ /**
338
+ * Throws when a classic start fresh could not delete the active run `runId`.
339
+ * Run before the commit prompt, as init does, so the user is not asked for a
340
+ * run that is then refused; deleteRunForStartFresh repeats the checks.
341
+ */
342
+ function checkRunForStartFresh(root, runId) {
343
+ (0, state_lock_1.withRunCreationLock)(root, () => {
344
+ const active = activeRunToDelete(root, runId);
345
+ refuseUndeletableRun(root, runId, active.state);
346
+ });
347
+ }
348
+ /**
349
+ * Deletes the record of the active run `runId` for a classic start fresh,
350
+ * under the refusals init applies before it replaces a run. The classic loop
351
+ * records no run of its own afterwards.
352
+ */
353
+ function deleteRunForStartFresh(root, runId) {
354
+ const deleted = (0, state_lock_1.withRunCreationLock)(root, () => {
355
+ activeRunToDelete(root, runId);
356
+ return deleteRunRecord(root, runId);
357
+ });
358
+ if (deleted) {
359
+ removeDeletedRunDir(root, runId);
360
+ }
361
+ }
362
+ // Call under the creation lock. A run that became active since the user chose
363
+ // is refused, never deleted in place of the one they saw.
364
+ function activeRunToDelete(root, runId) {
365
+ refuseLiveReservation(root);
366
+ const active = findActiveRunForInit(root, true);
367
+ if (!active) {
368
+ throw new Error(noActiveRunToReplace(runId));
369
+ }
370
+ if (active.runId !== runId) {
371
+ throw new Error(`Not starting fresh: migrate run '${active.runId}' became active while this command was starting. Re-run the command to see it.`);
372
+ }
373
+ return active;
374
+ }
317
375
  // Reads the newest active run; null when no run is active. Uninterpretable
318
376
  // run dirs refuse a fresh start (one of them could be an active run this
319
- // init would compete with) but only warn when a healthy active run is found,
320
- // unless `refuseUninterpretable`: a start-fresh would delete that run and
321
- // then find only the unreadable ones. NewerRunStateFormatError propagates
377
+ // invocation would compete with) but only warn when a healthy active run is
378
+ // found, unless `refuseUninterpretable`: a start-fresh would delete that run
379
+ // and then find only the unreadable ones. NewerRunStateFormatError propagates
322
380
  // from the read.
323
381
  function findActiveRunForInit(root, refuseUninterpretable = false) {
324
382
  const { active, uninterpretable } = (0, run_state_1.findActiveRun)(root);
@@ -331,7 +389,7 @@ function findActiveRunForInit(root, refuseUninterpretable = false) {
331
389
  const details = uninterpretable.map((u) => `${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${(0, text_1.singleLine)(u.dirName)}: ${(0, text_1.singleLine)(u.reason)}`);
332
390
  if (!active || refuseUninterpretable) {
333
391
  throw new Error([
334
- `Whether a migrate run is still active could not be determined; starting a new run could re-apply migrations an unfinished run already applied.`,
392
+ `Whether a migrate run is still active could not be determined; running the plan could re-apply migrations an unfinished run already applied.`,
335
393
  ...details,
336
394
  `Fix or remove the listed ${noun}, then re-run the command.`,
337
395
  ].join('\n'));
@@ -776,6 +834,8 @@ async function runOrchestratorReconcile(input) {
776
834
  return;
777
835
  }
778
836
  (0, give_up_1.warnAboutGiveUp)(target, outcome);
837
+ if (target.installFailed === true)
838
+ warnInstallLeftToUser(root, target);
779
839
  // Settled already; the dispense refuses a tree still held.
780
840
  scope.lease?.release();
781
841
  scope.lease = undefined;
@@ -786,8 +846,9 @@ async function runOrchestratorReconcile(input) {
786
846
  // sections stay synchronous); the transition and any unrecorded entry then
787
847
  // land in one write. Adopt and skip keep the tree, so the install they owe
788
848
  // runs here: the next dispense would take the changed deps as its baseline.
789
- // Giving up keeps it the same way when the run does not commit. Retries
790
- // owe nothing: the rearmed attempt reconciles itself.
849
+ // Giving up keeps it the same way when the run does not commit. An install
850
+ // that already failed is left to the step's mark instead. Retries owe
851
+ // nothing: the rearmed attempt reconciles itself.
791
852
  const { entry, installFailed, recorded } = await stepActionSideEffects(root, dir, state, target, stepAction, result.resetTree, scope);
792
853
  // A rearm starts a fresh attempt; drop the stale handoff before the rearm
793
854
  // is persisted so a crash in between can't refold the old outcome into the
@@ -863,6 +924,11 @@ async function runOrchestratorReconcile(input) {
863
924
  if (unresolvedArchiveError !== undefined) {
864
925
  (0, give_up_1.warnUnresolvedNotArchived)((0, state_machine_1.stepLabel)(target), unresolvedArchiveError);
865
926
  }
927
+ // A retry's rearm drops the mark: its next attempt installs.
928
+ if (target.installFailed === true &&
929
+ written.steps.find((s) => s.id === target.id)?.installFailed === true) {
930
+ warnInstallLeftToUser(root, target);
931
+ }
866
932
  state = written;
867
933
  }
868
934
  finally {
@@ -1131,6 +1197,14 @@ async function installFailedForStep(root, dir, state, step, seam, scope) {
1131
1197
  return true;
1132
1198
  }
1133
1199
  }
1200
+ function warnInstallLeftToUser(root, step) {
1201
+ (0, agent_output_1.warnToAgent)({
1202
+ title: `The dependency changes made by ${(0, state_machine_1.stepLabel)(step)} are not installed.`,
1203
+ bodyLines: [
1204
+ `Run \`${(0, util_1.pmInstallCommand)(root)}\` once the cause of the failed install is fixed. The run's final report repeats this unless a later install covers them.`,
1205
+ ],
1206
+ });
1207
+ }
1134
1208
  // A failed handoff fails the prompt; a success handoff completes it, unless it
1135
1209
  // marks the prompt not applicable via `extras.outcome === 'skipped'`.
1136
1210
  function handoffToPromptOutcome(handoff) {
@@ -1316,11 +1390,13 @@ function unresolvedResetsTree(state, step, cleanRetry) {
1316
1390
  async function stepActionSideEffects(root, dir, state, step, action, resetTree, scope) {
1317
1391
  switch (action) {
1318
1392
  case 'adopt':
1319
- // A died worker's unrecorded commit request is reused. A failed step, or
1320
- // a recorded commit, needs an adopt request of its own, or the old answer
1321
- // would be replayed.
1393
+ // A died worker's commit request is reused while nothing records its
1394
+ // outcome, since the session may still answer it. Otherwise the adopt
1395
+ // asks under its own request, or the old answer would be replayed.
1322
1396
  return state.createCommits
1323
1397
  ? commitForStep(root, dir, state, step, scope, step.status === 'failed' ||
1398
+ step.commitLedgerIndex !== undefined ||
1399
+ step.installFailed === true ||
1324
1400
  (0, state_machine_1.coveringLandedEntries)(state, step.id).length > 0
1325
1401
  ? 'adopt'
1326
1402
  : undefined)
@@ -1631,6 +1707,7 @@ function emitRetryFailed(root, runId, state, step, noProgress) {
1631
1707
  ` current HEAD: ${head ?? '(unknown)'}`,
1632
1708
  ` working tree: ${tree === null ? '(unknown)' : tree ? `\n${tree}` : '(clean)'}`,
1633
1709
  ``,
1710
+ ...unfinishedOperationLines(root, state, step),
1634
1711
  retryBudgetLine(step, committed),
1635
1712
  ``,
1636
1713
  `Decide how to proceed and re-run reconcile with one of:`,
@@ -1641,11 +1718,15 @@ function emitRetryFailed(root, runId, state, step, noProgress) {
1641
1718
  if (cleanRetry && !capReached) {
1642
1719
  lines.push(` retry-clean: reset the tree to ${step.gitRefBefore ?? 'the pre-migration ref'} (discarding its uncommitted tracked changes and the untracked files git does not ignore, ${types_1.MIGRATE_RUNS_RELATIVE_DIR} kept) and retry from that clean state by running: ${reconcileCommand(root, runId, 'retry-clean')}`);
1643
1720
  }
1721
+ // A failed commit is already named above the choices.
1722
+ const adoptWhen = recordedCommitFailed(state, step)
1723
+ ? ''
1724
+ : committed
1725
+ ? `a commit of this ${(0, state_machine_1.stepNoun)(step)}'s changes was started and never recorded; `
1726
+ : `the ${(0, state_machine_1.stepNoun)(step)} was ${step.kind === 'migration' ? 'applied' : 'done'} by hand; `;
1644
1727
  lines.push(landed
1645
1728
  ? ` adopt: keep the landed commit${landed.sha ? ` ${landed.sha}` : ''} and the current working-tree state as the ${(0, state_machine_1.stepNoun)(step)}'s result, then run: ${reconcileCommand(root, runId, 'adopt')}`
1646
- : ` adopt: ${committed
1647
- ? `a commit of this ${(0, state_machine_1.stepNoun)(step)}'s changes was started and never recorded`
1648
- : `the ${(0, state_machine_1.stepNoun)(step)} was ${step.kind === 'migration' ? 'applied' : 'done'} by hand`}; keep the current working-tree state as its result, then run: ${reconcileCommand(root, runId, 'adopt')}`);
1729
+ : ` adopt: ${adoptWhen}keep the current working-tree state as its result, then run: ${reconcileCommand(root, runId, 'adopt')}`);
1649
1730
  // Refused by the state machine: the migration is, or may be, committed.
1650
1731
  if (!committed) {
1651
1732
  lines.push(` skip: ${reconcileCommand(root, runId, 'skip')}`, unresolvedOptionLine(root, runId, state, step, unresolvedResetsTree(state, step, cleanRetry)));
@@ -1677,6 +1758,26 @@ function retryBudgetLine(step, committed) {
1677
1758
  ? `adopt only once you have inspected the tree and finished the ${(0, state_machine_1.stepNoun)(step)}; otherwise stop and report`
1678
1759
  : 'give the step up with unresolved and continue'}.`;
1679
1760
  }
1761
+ // A recorded failed commit with the mark standing withdraws skip and
1762
+ // unresolved, so every choice left commits.
1763
+ function unfinishedOperationLines(root, state, step) {
1764
+ const lines = [];
1765
+ if (step.installFailed === true) {
1766
+ lines.push(`Its dependency install did not complete. A retry runs it again, so fix the cause first if it can be fixed. Any other choice settles the step without installing and leaves the install to you; the run's final report asks for \`${(0, util_1.pmInstallCommand)(root)}\`.`, ``);
1767
+ }
1768
+ if (recordedCommitFailed(state, step)) {
1769
+ lines.push(`Its commit did not complete. Every choice below commits again.`, ``);
1770
+ }
1771
+ return lines;
1772
+ }
1773
+ function recordedCommitFailed(state, step) {
1774
+ // Not commitReceipt, which throws on an index the ledger lacks: a dispense
1775
+ // has to render whatever run.json holds.
1776
+ const receipt = step.commitLedgerIndex === undefined
1777
+ ? undefined
1778
+ : state.commits[step.commitLedgerIndex];
1779
+ return step.commitStarted === true && receipt?.kind === 'failed';
1780
+ }
1680
1781
  // Opens a capped dispense and is the reason a retry past the cap is refused.
1681
1782
  function rearmCapLine(step, committed) {
1682
1783
  return `This ${(0, state_machine_1.stepNoun)(step)} has already been retried ${step.attempt - 1} times without completing, and no further retry is accepted. ${committed
@@ -1776,6 +1877,7 @@ function emitDied(root, runId, state, step, noProgress) {
1776
1877
  ` current HEAD: ${head ?? '(unknown)'}`,
1777
1878
  ` working tree: ${tree === null ? '(unknown)' : tree ? `\n${tree}` : '(clean)'}`,
1778
1879
  ``,
1880
+ ...unfinishedOperationLines(root, state, step),
1779
1881
  retryBudgetLine(step, committed),
1780
1882
  ``,
1781
1883
  ];
@@ -1810,9 +1912,19 @@ function emitDied(root, runId, state, step, noProgress) {
1810
1912
  }, noProgress);
1811
1913
  }
1812
1914
  function emitHeld(root, runId, state, step, held, noProgress) {
1915
+ const heldStep = state.steps.find((s) => s.id === held.stepId);
1916
+ const heldStepName = heldStep && (0, state_machine_1.stepLabel)(heldStep);
1813
1917
  emit(root, runId, state, step, 'held', {
1814
1918
  next: reconcileCommand(root, runId),
1815
- instructionLines: [(0, broker_1.treeBusyMessage)(held)],
1919
+ instructionLines: [
1920
+ (0, broker_1.treeBusyMessage)(held, heldStepName),
1921
+ // Quitting this session frees the terminal only for its own parent.
1922
+ ...(held.owner === process.env[broker_1.BROKER_ENV_VAR]
1923
+ ? [
1924
+ `If it seems stuck, the user can ${(0, broker_1.sessionOperationWayOut)((0, broker_1.treeOperationLabel)(held, heldStepName))}.`,
1925
+ ]
1926
+ : []),
1927
+ ],
1816
1928
  }, noProgress);
1817
1929
  }
1818
1930
  function emitStillRunning(root, runId, state, step, held, noProgress) {
@@ -1830,7 +1942,7 @@ function emitStillRunning(root, runId, state, step, held, noProgress) {
1830
1942
  if (ageMs >= HANG_THRESHOLD_MS) {
1831
1943
  const hung = `It has been running for ${Math.floor(ageMs / 60000)} minutes and may be hung.`;
1832
1944
  lines.push(parentOperation
1833
- ? `${hung} Report this to the user: they can quit this session, then press Ctrl+C in the terminal to end ${parentOperation}, and resume the run afterwards.`
1945
+ ? `${hung} Report this to the user: they can ${(0, broker_1.sessionOperationWayOut)(parentOperation)}.`
1834
1946
  : `${hung} Verify pid ${step.pid}; either keep waiting, or kill it so the next reconcile can classify it as died.`);
1835
1947
  }
1836
1948
  emit(root, runId, state, step, 'still-running', {
@@ -2074,14 +2186,19 @@ function completionWarnings(root, runId, state) {
2074
2186
  // fact absorbed; suppress the warning only on a verified-clean tree. A dirty
2075
2187
  // tree can still be unrelated edits, so the warning only claims the changes
2076
2188
  // "may remain".
2077
- const commitDebt = (0, state_machine_1.hasPendingCommitDebt)(state) && (0, git_utils_1.getWorkingTreeStatus)(root) !== 'clean';
2189
+ const debtStepIds = (0, state_machine_1.uncoveredFailedStepIds)(state);
2190
+ const uncommitted = debtStepIds.length > 0 && (0, git_utils_1.getWorkingTreeStatus)(root) !== 'clean'
2191
+ ? state.steps.filter((s) => debtStepIds.includes(s.id))
2192
+ : [];
2078
2193
  const uninstalled = state.steps.filter((s) => s.installFailed);
2079
2194
  const issueLines = (0, issues_1.renderUnresolvedIssueLines)(state, runId);
2080
2195
  return [
2081
- ...(commitDebt
2196
+ ...(uncommitted.length > 0
2082
2197
  ? [
2083
2198
  [
2084
- 'Some migration changes could not be committed and may remain in the working tree; review and commit them manually.',
2199
+ `The changes made by ${uncommitted
2200
+ .map(state_machine_1.stepLabel)
2201
+ .join(', ')} could not be committed and may remain in the working tree; review and commit them manually.`,
2085
2202
  ],
2086
2203
  ]
2087
2204
  : []),
@@ -9,6 +9,19 @@ export declare const RUN_STATE_FILE_NAME = "run.json";
9
9
  * with fails closed as corrupt instead of being dispensed.
10
10
  */
11
11
  export declare const SHELL_SAFE_VALUE: RegExp;
12
+ export type MigrationIdProblem = 'not-shell-safe' | 'duplicate';
13
+ /**
14
+ * The ids of `migrations` that no orchestrated run can take, each once, in the
15
+ * order found. A worker finds its step by migration id, so an id the plan
16
+ * lists twice can't name one step.
17
+ */
18
+ export declare function migrationIdProblems(migrations: ReadonlyArray<{
19
+ package: string;
20
+ name: string;
21
+ }>): {
22
+ id: string;
23
+ problem: MigrationIdProblem;
24
+ }[];
12
25
  export declare const ISSUE_ID: RegExp;
13
26
  declare const MIGRATE_RUN_STATUSES: readonly ['active', 'completed'];
14
27
  export type MigrateRunStatus = (typeof MIGRATE_RUN_STATUSES)[number];
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NewerRunStateFormatError = exports.TREE_OPERATION_KINDS = exports.MIGRATE_ISSUE_DISPOSITIONS = exports.TERMINAL_STEP_STATUSES = exports.ISSUE_ID = exports.SHELL_SAFE_VALUE = exports.RUN_STATE_FILE_NAME = exports.CURRENT_RUN_STATE_FORMAT_VERSION = void 0;
4
+ exports.migrationIdProblems = migrationIdProblems;
4
5
  exports.issueFingerprint = issueFingerprint;
5
6
  exports.migrateRunsDir = migrateRunsDir;
6
7
  exports.runDir = runDir;
@@ -32,6 +33,26 @@ exports.RUN_STATE_FILE_NAME = 'run.json';
32
33
  * with fails closed as corrupt instead of being dispensed.
33
34
  */
34
35
  exports.SHELL_SAFE_VALUE = /^[A-Za-z0-9@/:._-]+$/;
36
+ /**
37
+ * The ids of `migrations` that no orchestrated run can take, each once, in the
38
+ * order found. A worker finds its step by migration id, so an id the plan
39
+ * lists twice can't name one step.
40
+ */
41
+ function migrationIdProblems(migrations) {
42
+ const seen = new Set();
43
+ const problems = new Map();
44
+ for (const m of migrations) {
45
+ const id = `${m.package}:${m.name}`;
46
+ if (!exports.SHELL_SAFE_VALUE.test(id)) {
47
+ problems.set(id, 'not-shell-safe');
48
+ }
49
+ else if (seen.has(id) && !problems.has(id)) {
50
+ problems.set(id, 'duplicate');
51
+ }
52
+ seen.add(id);
53
+ }
54
+ return [...problems].map(([id, problem]) => ({ id, problem }));
55
+ }
35
56
  // `new Date().toISOString()`, the only shape Nx writes. Retention and active-run
36
57
  // selection compare these lexicographically, and the value is rendered into the
37
58
  // stdout the agent scans for blocks, so neither a different notation nor an
@@ -76,6 +76,25 @@ function renderRunbook(ctx) {
76
76
  `already active. Show its report to the user and let them choose between`,
77
77
  `the two commands it lists; do not choose for them.`,
78
78
  ``,
79
+ `## Long-running commands`,
80
+ ``,
81
+ `A worker or \`next\` command can wait for minutes on a dependency install`,
82
+ `or a commit. When Nx started this session, the nx process that started it`,
83
+ `runs them, and the waiting command prints an \`[nx]\` line after 15`,
84
+ `seconds and then once a minute: what nx is running and for how long.`,
85
+ ``,
86
+ `- If your shell tool can run a command in the background, start worker and`,
87
+ ` \`next\` commands that way and check their output about once a minute.`,
88
+ ` Each time, tell the user in one sentence what the latest \`[nx]\` line`,
89
+ ` says.`,
90
+ `- Claude Code: start each worker and \`next\` command with`,
91
+ ` \`run_in_background\`, then start a Monitor on its output that passes`,
92
+ ` \`[nx]\` lines through. A \`next\` command can wait as long as a worker.`,
93
+ `- Stopping a worker or \`next\` command does not stop the install or the`,
94
+ ` commit it waits on. If one seems stuck, tell the user they can quit this`,
95
+ ` session, then press Ctrl+C in the terminal to end it, and resume the run`,
96
+ ` afterwards.`,
97
+ ``,
79
98
  `## Agent work`,
80
99
  ``,
81
100
  `Some steps hand work back to you:`,
@@ -231,7 +231,10 @@ function applyStepAction(state, index, step, action) {
231
231
  };
232
232
  }
233
233
  function workerDiedSummary(step) {
234
- return `the worker process${step.pid === undefined ? '' : ` (pid ${step.pid})`} died before recording an outcome`;
234
+ const worker = `the worker process${step.pid === undefined ? '' : ` (pid ${step.pid})`}`;
235
+ return step.installFailed === true
236
+ ? `its dependency install did not complete, and ${worker} died`
237
+ : `${worker} died before recording an outcome`;
235
238
  }
236
239
  // Re-arms for a retry that resets the tree first. The reset target predates
237
240
  // the generator unless a commit of this step's own already carries it, so the
@@ -28,6 +28,9 @@ export declare function depsHash(root: string): string | null;
28
28
  * dispense-time probe failed has no baseline, which says the dependencies it
29
29
  * started from are unknown rather than unchanged, and skipping there would
30
30
  * strand the change with nothing left to detect it.
31
+ *
32
+ * A failed or died step whose install already failed settles without it, so
33
+ * the settle cannot hang on that install again. A retry's rearm drops the mark.
31
34
  */
32
35
  export declare function installDepsChangedSinceDispense(root: string, dir: string, step: MigrateStep, skipInstall: boolean, rerunCommand?: string, sink?: MigrateOutputSink): Promise<void>;
33
36
  /**
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- // Internal to run/: deliberately not re-exported from ./index.
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
3
  exports.nowIso = nowIso;
5
4
  exports.depsHash = depsHash;
@@ -51,8 +50,15 @@ function depsHash(root) {
51
50
  * dispense-time probe failed has no baseline, which says the dependencies it
52
51
  * started from are unknown rather than unchanged, and skipping there would
53
52
  * strand the change with nothing left to detect it.
53
+ *
54
+ * A failed or died step whose install already failed settles without it, so
55
+ * the settle cannot hang on that install again. A retry's rearm drops the mark.
54
56
  */
55
57
  async function installDepsChangedSinceDispense(root, dir, step, skipInstall, rerunCommand, sink) {
58
+ if (step.installFailed === true &&
59
+ (step.status === 'failed' || step.status === 'died')) {
60
+ return;
61
+ }
56
62
  const current = depsHash(root);
57
63
  if (current !== null && current === step.depsHashAtDispense)
58
64
  return;
@@ -5,12 +5,6 @@ interface SortableMigration {
5
5
  }
6
6
  /**
7
7
  * Sorts in place (like `Array.prototype.sort`) and returns the same array.
8
- *
9
- * `hoistHandoffGitignore` is set for agentic runs, which write scratch under
10
- * `.nx/migrate-runs`; see `agentic/handoff-gitignore.ts` for why the v23
11
- * gitignore migration must run first there.
12
8
  */
13
- export declare function sortMigrations<T extends SortableMigration>(migrations: T[], opts: {
14
- hoistHandoffGitignore: boolean;
15
- }): T[];
9
+ export declare function sortMigrations<T extends SortableMigration>(migrations: T[]): T[];
16
10
  export {};
@@ -2,23 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sortMigrations = sortMigrations;
4
4
  const semver_1 = require("semver");
5
- const types_1 = require("./agentic/types");
6
5
  const version_utils_1 = require("./version-utils");
7
6
  /**
8
7
  * Sorts in place (like `Array.prototype.sort`) and returns the same array.
9
- *
10
- * `hoistHandoffGitignore` is set for agentic runs, which write scratch under
11
- * `.nx/migrate-runs`; see `agentic/handoff-gitignore.ts` for why the v23
12
- * gitignore migration must run first there.
13
8
  */
14
- function sortMigrations(migrations, opts) {
9
+ function sortMigrations(migrations) {
15
10
  return migrations.sort((a, b) => {
16
- if (opts.hoistHandoffGitignore) {
17
- if ((0, types_1.isHandoffGitignoreMigration)(a))
18
- return -1;
19
- if ((0, types_1.isHandoffGitignoreMigration)(b))
20
- return 1;
21
- }
22
11
  if (a.name === '15-7-0-split-configuration-into-project-json-files') {
23
12
  return -1;
24
13
  }
@@ -126,7 +126,7 @@ function resolveTaskGraphDependencies(graph, extraTargetDeps, projectName, targe
126
126
  const directDeps = [
127
127
  ...(taskGraph.dependencies[rootId] ?? []),
128
128
  ...(taskGraph.continuousDependencies[rootId] ?? []),
129
- ];
129
+ ].map((edge) => edge.id);
130
130
  const directDepSet = new Set(directDeps);
131
131
  const depSourceIndices = directDeps.map((depTaskId) => {
132
132
  const task = taskGraph.tasks[depTaskId];
@@ -711,7 +711,9 @@ export interface NxSyncConfiguration {
711
711
  export interface NxMigrateConfiguration {
712
712
  /**
713
713
  * Whether to automatically create a git commit after each migration runs.
714
- * Equivalent to the `--create-commits` flag. Defaults to `false`.
714
+ * Equivalent to the `--create-commits` flag. Defaults to `true` when the
715
+ * agentic flow runs the migrations in a Git repository, and `false`
716
+ * otherwise.
715
717
  */
716
718
  createCommits?: boolean;
717
719
  /**
@@ -753,11 +755,18 @@ export interface NxMigrateConfiguration {
753
755
  */
754
756
  agentic?: boolean | AgentId;
755
757
  /**
756
- * Whether to run agent-driven validation after generator-only migrations when
757
- * the agentic flow is enabled. Equivalent to the `--validate` flag. Defaults
758
- * to `true` when the agentic flow is enabled.
758
+ * Whether the AI agent driving a migrate run reviews the changes of
759
+ * generator-only migrations. Equivalent to the `--validate` flag. Defaults to
760
+ * `true`.
759
761
  */
760
762
  validate?: boolean;
763
+ /**
764
+ * Whether a migrate run an AI agent drives ends with a final check, where the
765
+ * agent makes sure the projects the run touched still lint, build, and pass
766
+ * their unit tests. Equivalent to the `--final-validation` flag. Defaults to
767
+ * `true`.
768
+ */
769
+ finalValidation?: boolean;
761
770
  }
762
771
  /**
763
772
  * Nx.json configuration
@@ -3,4 +3,4 @@
3
3
  * `packages/nx/src/native/tasks/types.rs` and exposed to TypeScript via NAPI.
4
4
  * This file re-exports them so existing imports keep working.
5
5
  */
6
- export type { Task, TaskGraph, TaskTarget, TaskHashDetails, TaskUltracacheSettings, } from '../native';
6
+ export type { Task, TaskGraph, TaskGraphEdge, TaskTarget, TaskHashDetails, TaskUltracacheSettings, } from '../native';
@@ -163,6 +163,54 @@ export type Spreadable<T> = T & {
163
163
  * permits `'...'`, which target merging resolves away.
164
164
  */
165
165
  export type TargetUltracacheSettings = Spreadable<TaskUltracacheSettings>;
166
+ export interface ReadyWhenOptions {
167
+ /**
168
+ * Milliseconds to wait for readiness before failing dependents that wait
169
+ * for it. Default is 60000.
170
+ */
171
+ timeout?: number;
172
+ /**
173
+ * Milliseconds between probe attempts. By default attempts back off from
174
+ * 100 to 1000 milliseconds. Unused by `logMatches`.
175
+ */
176
+ interval?: number;
177
+ }
178
+ /**
179
+ * Readiness probe of a continuous target. Exactly one of `url`, `port`,
180
+ * `command` or `logMatches` must be set. Strings may use the `{workspaceRoot}`,
181
+ * `{projectRoot}`, `{projectName}` and `{options.*}` placeholders, resolved
182
+ * against the options the task runs with.
183
+ */
184
+ export type ReadyWhen = ({
185
+ /**
186
+ * Ready when an HTTP or HTTPS GET gets a status from 200 to 403. A
187
+ * redirect counts as ready and is not followed.
188
+ */
189
+ url: string;
190
+ } & ReadyWhenOptions) | ({
191
+ /**
192
+ * Ready when a TCP connection to the port succeeds. A string must
193
+ * resolve to a port number through a placeholder such as
194
+ * `{options.port}`.
195
+ */
196
+ port: number | string;
197
+ /**
198
+ * Host to connect to. By default 127.0.0.1 and ::1 are both tried.
199
+ */
200
+ host?: string;
201
+ } & ReadyWhenOptions) | ({
202
+ /**
203
+ * Ready when the command, run through the shell at the workspace root,
204
+ * exits with code 0.
205
+ */
206
+ command: string;
207
+ } & ReadyWhenOptions) | ({
208
+ /**
209
+ * Ready when every one of these substrings has appeared in the task's
210
+ * output.
211
+ */
212
+ logMatches: string | string[];
213
+ } & ReadyWhenOptions);
166
214
  export interface TargetDependencyConfig {
167
215
  /**
168
216
  * A list of projects that have `target`.
@@ -192,22 +240,40 @@ export interface TargetDependencyConfig {
192
240
  * target's hash. It still runs first. Has no effect on other dependencies.
193
241
  */
194
242
  inputs?: boolean;
243
+ /**
244
+ * When the dependency is a continuous task, whether this target waits for
245
+ * it to have started (default) or to be ready per its `readyWhen`.
246
+ * Ignored when the dependency is not continuous or declares no `readyWhen`.
247
+ */
248
+ waitFor?: 'started' | 'ready';
195
249
  }
250
+ /**
251
+ * `always`: the files the input resolves to stay in the hash even when the task
252
+ * is hashed from its ultracache recording.
253
+ * `dependencies` with `projects`: the selected projects' dependencies (as `^input`
254
+ * resolves for each), not the projects themselves.
255
+ */
196
256
  export type InputDefinition = {
197
257
  input: string;
198
258
  projects: string | string[];
259
+ dependencies?: boolean;
260
+ always?: boolean;
199
261
  } | {
200
262
  input: string;
201
263
  dependencies: true;
264
+ always?: boolean;
202
265
  } | {
203
266
  input: string;
267
+ always?: boolean;
204
268
  } | {
205
269
  fileset: string;
206
270
  includeIgnored?: boolean;
271
+ always?: boolean;
207
272
  } | {
208
273
  fileset: string;
209
274
  dependencies: true;
210
275
  includeIgnored?: boolean;
276
+ always?: boolean;
211
277
  } | {
212
278
  runtime: string;
213
279
  } | {
@@ -282,6 +348,12 @@ export interface TargetConfiguration<T = any> {
282
348
  * Whether this target runs continuously
283
349
  */
284
350
  continuous?: boolean;
351
+ /**
352
+ * How Nx detects that this continuous target is ready. Only honored when
353
+ * `continuous` is true; dependents opt in with `waitFor: 'ready'` on their
354
+ * `dependsOn` entry.
355
+ */
356
+ readyWhen?: ReadyWhen;
285
357
  /**
286
358
  * List of generators to run before the target to ensure the workspace
287
359
  * is up to date.