nx 23.3.0-beta.7 → 23.3.0-beta.9

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 (138) hide show
  1. package/README.md +1 -1
  2. package/dist/nx-cloud-internals.d.ts +1 -1
  3. package/dist/nx-cloud-internals.js +4 -4
  4. package/dist/src/command-line/affected/affected.js +20 -1
  5. package/dist/src/command-line/affected/command-object.js +1 -1
  6. package/dist/src/command-line/migrate/agentic/master/run-master-session.d.ts +6 -2
  7. package/dist/src/command-line/migrate/agentic/master/run-master-session.js +88 -14
  8. package/dist/src/command-line/migrate/command-object.d.ts +1 -0
  9. package/dist/src/command-line/migrate/command-object.js +6 -1
  10. package/dist/src/command-line/migrate/migrate-analytics.d.ts +34 -9
  11. package/dist/src/command-line/migrate/migrate-analytics.js +71 -10
  12. package/dist/src/command-line/migrate/migrate-config.d.ts +5 -2
  13. package/dist/src/command-line/migrate/migrate-config.js +19 -11
  14. package/dist/src/command-line/migrate/migrate.d.ts +2 -0
  15. package/dist/src/command-line/migrate/migrate.js +152 -45
  16. package/dist/src/command-line/migrate/resolve-package-version.js +2 -19
  17. package/dist/src/command-line/migrate/run/broker.d.ts +14 -2
  18. package/dist/src/command-line/migrate/run/broker.js +95 -34
  19. package/dist/src/command-line/migrate/run/existing-run-report.d.ts +55 -0
  20. package/dist/src/command-line/migrate/run/existing-run-report.js +225 -0
  21. package/dist/src/command-line/migrate/run/give-up.d.ts +41 -0
  22. package/dist/src/command-line/migrate/run/give-up.js +148 -0
  23. package/dist/src/command-line/migrate/run/index.d.ts +6 -4
  24. package/dist/src/command-line/migrate/run/index.js +15 -2
  25. package/dist/src/command-line/migrate/run/issues.d.ts +14 -0
  26. package/dist/src/command-line/migrate/run/issues.js +59 -0
  27. package/dist/src/command-line/migrate/run/orchestrator.d.ts +40 -1
  28. package/dist/src/command-line/migrate/run/orchestrator.js +509 -160
  29. package/dist/src/command-line/migrate/run/run-id.d.ts +0 -8
  30. package/dist/src/command-line/migrate/run/run-id.js +0 -30
  31. package/dist/src/command-line/migrate/run/run-state.d.ts +11 -9
  32. package/dist/src/command-line/migrate/run/run-state.js +25 -12
  33. package/dist/src/command-line/migrate/run/runbook.js +9 -4
  34. package/dist/src/command-line/migrate/run/state-lock.d.ts +31 -0
  35. package/dist/src/command-line/migrate/run/state-lock.js +126 -0
  36. package/dist/src/command-line/migrate/run/state-machine.d.ts +15 -0
  37. package/dist/src/command-line/migrate/run/state-machine.js +137 -10
  38. package/dist/src/command-line/migrate/run/util.d.ts +1 -0
  39. package/dist/src/command-line/migrate/run/util.js +21 -0
  40. package/dist/src/command-line/migrate/run/worker.js +3 -0
  41. package/dist/src/command-line/migrate/step-actions.d.ts +1 -1
  42. package/dist/src/command-line/migrate/step-actions.js +7 -1
  43. package/dist/src/command-line/migrate/text.d.ts +6 -0
  44. package/dist/src/command-line/migrate/text.js +25 -0
  45. package/dist/src/command-line/release/utils/release-graph.js +1 -1
  46. package/dist/src/command-line/release/utils/remote-release-clients/github.js +2 -5
  47. package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.d.ts +2 -2
  48. package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.js +4 -5
  49. package/dist/src/command-line/release/version/version-actions.d.ts +3 -0
  50. package/dist/src/command-line/reset/reset.js +10 -4
  51. package/dist/src/command-line/show/command-object.js +1 -1
  52. package/dist/src/command-line/show/projects.js +22 -0
  53. package/dist/src/command-line/yargs-utils/shared-options.d.ts +4 -0
  54. package/dist/src/command-line/yargs-utils/shared-options.js +16 -0
  55. package/dist/src/config/task-graph.d.ts +1 -1
  56. package/dist/src/config/workspace-json-project-json.d.ts +4 -4
  57. package/dist/src/daemon/client/client.d.ts +3 -3
  58. package/dist/src/daemon/client/client.js +4 -4
  59. package/dist/src/daemon/message-types/select-affected-tasks.d.ts +3 -0
  60. package/dist/src/daemon/message-types/ultracache-configuration-version.d.ts +8 -0
  61. package/dist/src/daemon/server/handle-hash-tasks.d.ts +2 -2
  62. package/dist/src/daemon/server/handle-hash-tasks.js +3 -3
  63. package/dist/src/daemon/server/handle-select-affected-tasks.js +5 -2
  64. package/dist/src/daemon/server/ultracache-configuration-state.d.ts +7 -0
  65. package/dist/src/daemon/server/{io-snapshots-state.js → ultracache-configuration-state.js} +5 -4
  66. package/dist/src/devkit-internals.d.ts +3 -1
  67. package/dist/src/devkit-internals.js +8 -3
  68. package/dist/src/hasher/create-task-hasher.d.ts +3 -3
  69. package/dist/src/hasher/create-task-hasher.js +6 -6
  70. package/dist/src/hasher/hash-task.d.ts +2 -2
  71. package/dist/src/hasher/hash-task.js +6 -5
  72. package/dist/src/hasher/native-task-hasher-impl.d.ts +7 -7
  73. package/dist/src/hasher/native-task-hasher-impl.js +19 -19
  74. package/dist/src/hasher/task-hasher.d.ts +14 -14
  75. package/dist/src/hasher/task-hasher.js +13 -13
  76. package/dist/src/native/index.d.ts +151 -102
  77. package/dist/src/native/native-bindings.js +4 -5
  78. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  79. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  80. package/dist/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.js +15 -8
  81. package/dist/src/nx-cloud/update-manager.d.ts +3 -3
  82. package/dist/src/nx-cloud/update-manager.js +19 -13
  83. package/dist/src/nx-cloud/utilities/client.d.ts +1 -1
  84. package/dist/src/nx-cloud/utilities/client.js +1 -1
  85. package/dist/src/nx-cloud/utilities/is-workspace-claimed.js +2 -1
  86. package/dist/src/nx-cloud/utilities/nx-cloud-http-client.d.ts +5 -0
  87. package/dist/src/nx-cloud/utilities/nx-cloud-http-client.js +16 -0
  88. package/dist/src/nx-cloud/utilities/url-shorten.js +17 -10
  89. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +22 -18
  90. package/dist/src/plugins/js/utils/register.js +11 -3
  91. package/dist/src/project-graph/affected/affected-reasons.d.ts +116 -0
  92. package/dist/src/project-graph/affected/affected-reasons.js +459 -0
  93. package/dist/src/project-graph/affected/affected-tasks.d.ts +23 -10
  94. package/dist/src/project-graph/affected/affected-tasks.js +203 -31
  95. package/dist/src/project-graph/affected/print-explanation.d.ts +11 -0
  96. package/dist/src/project-graph/affected/print-explanation.js +44 -0
  97. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  98. package/dist/src/tasks-runner/init-tasks-runner.d.ts +5 -5
  99. package/dist/src/tasks-runner/init-tasks-runner.js +8 -15
  100. package/dist/src/tasks-runner/run-command.d.ts +7 -7
  101. package/dist/src/tasks-runner/run-command.js +19 -23
  102. package/dist/src/tasks-runner/task-orchestrator.d.ts +7 -3
  103. package/dist/src/tasks-runner/task-orchestrator.js +9 -8
  104. package/dist/src/tasks-runner/tasks-runner.d.ts +2 -0
  105. package/dist/src/ultracache/config.d.ts +21 -0
  106. package/dist/src/ultracache/config.js +29 -0
  107. package/dist/src/ultracache/fetch.d.ts +21 -0
  108. package/dist/src/{io-snapshots → ultracache}/fetch.js +9 -9
  109. package/dist/src/ultracache/overrides.d.ts +17 -0
  110. package/dist/src/{io-snapshots → ultracache}/overrides.js +14 -13
  111. package/dist/src/ultracache/report.d.ts +13 -0
  112. package/dist/src/{io-snapshots → ultracache}/report.js +15 -21
  113. package/dist/src/ultracache/store.d.ts +40 -0
  114. package/dist/src/{io-snapshots → ultracache}/store.js +45 -45
  115. package/dist/src/utils/ab-testing.js +11 -12
  116. package/dist/src/utils/command-line-utils.d.ts +1 -0
  117. package/dist/src/utils/create-cli-options.d.ts +6 -0
  118. package/dist/src/utils/create-cli-options.js +29 -0
  119. package/dist/src/utils/db-connection.d.ts +1 -0
  120. package/dist/src/utils/db-connection.js +1 -0
  121. package/dist/src/utils/git-utils.d.ts +7 -0
  122. package/dist/src/utils/git-utils.js +21 -9
  123. package/dist/src/utils/http-client.d.ts +27 -0
  124. package/dist/src/utils/http-client.js +193 -0
  125. package/package.json +12 -38
  126. package/dist/src/daemon/message-types/io-snapshot-version.d.ts +0 -5
  127. package/dist/src/daemon/server/io-snapshots-state.d.ts +0 -7
  128. package/dist/src/io-snapshots/config.d.ts +0 -23
  129. package/dist/src/io-snapshots/config.js +0 -32
  130. package/dist/src/io-snapshots/fetch.d.ts +0 -21
  131. package/dist/src/io-snapshots/outputs.d.ts +0 -12
  132. package/dist/src/io-snapshots/outputs.js +0 -33
  133. package/dist/src/io-snapshots/overrides.d.ts +0 -17
  134. package/dist/src/io-snapshots/report.d.ts +0 -13
  135. package/dist/src/io-snapshots/store.d.ts +0 -37
  136. package/dist/src/nx-cloud/utilities/axios.d.ts +0 -4
  137. package/dist/src/nx-cloud/utilities/axios.js +0 -17
  138. /package/dist/src/daemon/message-types/{io-snapshot-version.js → ultracache-configuration-version.js} +0 -0
@@ -6,11 +6,3 @@ export declare const RUN_ID_SAFE: RegExp;
6
6
  * mid-run.
7
7
  */
8
8
  export declare function createRunId(): string;
9
- /**
10
- * Hashes a migrations.json plan so a resumed run can detect whether the plan
11
- * changed since a round was recorded. `nx-console` is stripped first since
12
- * editors write to it without changing the plan; object keys are sorted
13
- * recursively (arrays keep their order) so key reordering from a different
14
- * JSON serializer doesn't change the hash.
15
- */
16
- export declare function computePlanHash(migrationsJsonContent: string | object): string;
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RUN_ID_SAFE = void 0;
4
4
  exports.createRunId = createRunId;
5
- exports.computePlanHash = computePlanHash;
6
5
  const crypto_1 = require("crypto");
7
6
  // Orchestrator-generated run ids always match; anything else could smuggle
8
7
  // shell metacharacters into dispensed commands or a path out of the runs dir
@@ -25,32 +24,3 @@ function compactUtcTimestamp(date) {
25
24
  .replace(/[-:]/g, '')
26
25
  .replace(/\.\d+Z$/, '');
27
26
  }
28
- /**
29
- * Hashes a migrations.json plan so a resumed run can detect whether the plan
30
- * changed since a round was recorded. `nx-console` is stripped first since
31
- * editors write to it without changing the plan; object keys are sorted
32
- * recursively (arrays keep their order) so key reordering from a different
33
- * JSON serializer doesn't change the hash.
34
- */
35
- function computePlanHash(migrationsJsonContent) {
36
- const parsed = (typeof migrationsJsonContent === 'string'
37
- ? JSON.parse(migrationsJsonContent)
38
- : migrationsJsonContent);
39
- const withoutNxConsole = Object.fromEntries(Object.entries(parsed).filter(([key]) => key !== 'nx-console'));
40
- return (0, crypto_1.createHash)('sha256')
41
- .update(JSON.stringify(canonicalize(withoutNxConsole)))
42
- .digest('hex');
43
- }
44
- function canonicalize(value) {
45
- if (Array.isArray(value)) {
46
- return value.map(canonicalize);
47
- }
48
- if (value !== null && typeof value === 'object') {
49
- const sorted = {};
50
- for (const key of Object.keys(value).sort()) {
51
- sorted[key] = canonicalize(value[key]);
52
- }
53
- return sorted;
54
- }
55
- return value;
56
- }
@@ -14,10 +14,9 @@ declare const MIGRATE_RUN_STATUSES: readonly ['active', 'completed'];
14
14
  export type MigrateRunStatus = (typeof MIGRATE_RUN_STATUSES)[number];
15
15
  export interface MigrateRunRound {
16
16
  index: number;
17
- planHash: string;
18
17
  planSnapshot: string;
19
18
  }
20
- declare const MIGRATE_STEP_STATUSES: readonly ['pending', 'dispensed', 'running', 'awaiting-prompt-outcome', 'succeeded', 'failed', 'skipped', 'died'];
19
+ declare const MIGRATE_STEP_STATUSES: readonly ['pending', 'dispensed', 'running', 'awaiting-prompt-outcome', 'succeeded', 'failed', 'skipped', 'died', 'unresolved'];
21
20
  export type MigrateStepStatus = (typeof MIGRATE_STEP_STATUSES)[number];
22
21
  export declare const TERMINAL_STEP_STATUSES: ReadonlySet<MigrateStepStatus>;
23
22
  declare const PROMPT_OUTCOME_STATUSES: readonly ['completed', 'skipped', 'failed'];
@@ -50,6 +49,8 @@ export interface MigrateStep {
50
49
  depsHashAtDispense?: string;
51
50
  outcome?: MigrateStepOutcome;
52
51
  promptOutcome?: MigrateStepPromptOutcome;
52
+ adopted?: boolean;
53
+ unresolvedIssueId?: string;
53
54
  awaitingKind?: MigrateStepAwaitingKind;
54
55
  commitLedgerIndex?: number;
55
56
  commitStarted?: boolean;
@@ -112,6 +113,7 @@ export interface MigrateRunState {
112
113
  skipInstall?: boolean;
113
114
  validate?: boolean;
114
115
  runbookPath?: string;
116
+ branch?: string;
115
117
  rounds: MigrateRunRound[];
116
118
  steps: MigrateStep[];
117
119
  commits: MigrateCommitLedgerEntry[];
@@ -121,7 +123,7 @@ export interface MigrateRunState {
121
123
  treeOperation?: MigrateTreeOperation;
122
124
  analytics: MigrateRunAnalytics;
123
125
  }
124
- export declare const TREE_OPERATION_KINDS: readonly ['commit', 'install', 'fold-install', 'action-install', 'reset', 'checkpoint'];
126
+ export declare const TREE_OPERATION_KINDS: readonly ['commit', 'install', 'fold-install', 'action-install', 'reset', 'give-up', 'checkpoint'];
125
127
  export type MigrateTreeOperationKind = (typeof TREE_OPERATION_KINDS)[number];
126
128
  export interface MigrateTreeOperation {
127
129
  kind: MigrateTreeOperationKind;
@@ -135,15 +137,14 @@ export declare function runDir(root: string, runId: string): string;
135
137
  /** See `HANDOFFS_DIR_NAME` for why the subtree exists. */
136
138
  export declare function runHandoffsDir(runDirPath: string): string;
137
139
  /**
138
- * Thrown when a run.json declares a `formatVersion` newer than this Nx
140
+ * Thrown for a run.json whose `formatVersion` is newer than this Nx
139
141
  * understands. Callers must not treat such a run as absent: an older Nx
140
142
  * ignoring a newer active run would start a competing run on top of it.
141
143
  *
142
- * Adding a member to any persisted closed set (run status, step status,
143
- * awaiting kind, prompt-outcome status, commit kind, issue disposition) needs a
144
- * `CURRENT_RUN_STATE_FORMAT_VERSION` bump: without it, an older Nx reading
145
- * the new value would reject the run as corrupt (the closed-set validation
146
- * fails) instead of refusing with this error's ask for a newer Nx.
144
+ * Adding a member to a persisted closed set needs a
145
+ * `CURRENT_RUN_STATE_FORMAT_VERSION` bump only once a released Nx can read the
146
+ * run, or that Nx rejects the new value as corruption instead of asking for a
147
+ * newer Nx.
147
148
  */
148
149
  export declare class NewerRunStateFormatError extends Error {
149
150
  constructor(message: string);
@@ -191,6 +192,7 @@ export declare function findActiveRun(root: string): {
191
192
  runId: string;
192
193
  state: MigrateRunState;
193
194
  } | null;
195
+ activeRunIds: string[];
194
196
  uninterpretable: UninterpretableRunDir[];
195
197
  };
196
198
  /**
@@ -82,12 +82,13 @@ const MIGRATE_STEP_STATUSES = [
82
82
  'failed',
83
83
  'skipped',
84
84
  'died',
85
+ 'unresolved',
85
86
  ];
86
- // 'failed' and 'died' are not terminal: both can be re-armed into a fresh
87
- // attempt.
87
+ // 'failed' and 'died' can be re-armed; 'unresolved' cannot.
88
88
  exports.TERMINAL_STEP_STATUSES = new Set([
89
89
  'succeeded',
90
90
  'skipped',
91
+ 'unresolved',
91
92
  ]);
92
93
  const PROMPT_OUTCOME_STATUSES = ['completed', 'skipped', 'failed'];
93
94
  const MIGRATE_STEP_AWAITING_KINDS = [
@@ -118,6 +119,7 @@ exports.TREE_OPERATION_KINDS = [
118
119
  'fold-install',
119
120
  'action-install',
120
121
  'reset',
122
+ 'give-up',
121
123
  'checkpoint',
122
124
  ];
123
125
  const REQUIRED_TOP_LEVEL_FIELDS = [
@@ -192,7 +194,6 @@ function isStepIdArray(value) {
192
194
  function isRoundShape(value) {
193
195
  return (isPlainObject(value) &&
194
196
  typeof value.index === 'number' &&
195
- typeof value.planHash === 'string' &&
196
197
  typeof value.planSnapshot === 'string' &&
197
198
  PLAN_SNAPSHOT_NAME.test(value.planSnapshot));
198
199
  }
@@ -251,6 +252,11 @@ function isStepShape(value) {
251
252
  isOptionalBoolean(value.validationOwed) &&
252
253
  isOptionalBoolean(value.generatorMadeChanges) &&
253
254
  isOptionalBoolean(value.installFailed) &&
255
+ isOptionalBoolean(value.adopted) &&
256
+ isOptionalMatching(exports.ISSUE_ID, value.unresolvedIssueId) &&
257
+ // Minted by the same write that gives the step up, so it never names a
258
+ // step in any other status.
259
+ (value.unresolvedIssueId === undefined || value.status === 'unresolved') &&
254
260
  // A cross-field invariant the rest of the loop relies on: a running step
255
261
  // without a pid is never reclassified as died and no step action targets
256
262
  // it, so it stalls the run forever.
@@ -385,6 +391,7 @@ function hasValidRunStateShape(parsed) {
385
391
  isOptionalBoolean(parsed.skipInstall) &&
386
392
  isOptionalBoolean(parsed.validate) &&
387
393
  isOptionalMatching(RUNBOOK_NAME, parsed.runbookPath) &&
394
+ isOptionalString(parsed.branch) &&
388
395
  parsed.rounds.every(isRoundShape) &&
389
396
  parsed.steps.every(isStepShape) &&
390
397
  hasUniqueStepIds(parsed.steps) &&
@@ -398,24 +405,28 @@ function hasValidRunStateShape(parsed) {
398
405
  // Written only for issues that already existed: a dangling id could make a
399
406
  // future issue's resolution look carried by a commit that predates it.
400
407
  parsed.commits.every((c) => c.issueIds === undefined ||
401
- c.issueIds.every((id) => (parsed.issues ?? []).some((i) => i.id === id))) &&
408
+ c.issueIds.every((id) => hasIssueWithId(parsed, id))) &&
409
+ parsed.steps.every((s) => s.unresolvedIssueId === undefined ||
410
+ hasIssueWithId(parsed, s.unresolvedIssueId)) &&
402
411
  isNoProgressShape(parsed.noProgress) &&
403
412
  isTreeOperationShape(parsed.treeOperation) &&
404
413
  isAnalyticsShape(parsed.analytics));
405
414
  }
415
+ function hasIssueWithId(parsed, id) {
416
+ return (parsed.issues ?? []).some((i) => i.id === id);
417
+ }
406
418
  function corruptRunStateError(filePath, reason) {
407
419
  return new Error(`Corrupt run state at ${filePath}: ${reason}`);
408
420
  }
409
421
  /**
410
- * Thrown when a run.json declares a `formatVersion` newer than this Nx
422
+ * Thrown for a run.json whose `formatVersion` is newer than this Nx
411
423
  * understands. Callers must not treat such a run as absent: an older Nx
412
424
  * ignoring a newer active run would start a competing run on top of it.
413
425
  *
414
- * Adding a member to any persisted closed set (run status, step status,
415
- * awaiting kind, prompt-outcome status, commit kind, issue disposition) needs a
416
- * `CURRENT_RUN_STATE_FORMAT_VERSION` bump: without it, an older Nx reading
417
- * the new value would reject the run as corrupt (the closed-set validation
418
- * fails) instead of refusing with this error's ask for a newer Nx.
426
+ * Adding a member to a persisted closed set needs a
427
+ * `CURRENT_RUN_STATE_FORMAT_VERSION` bump only once a released Nx can read the
428
+ * run, or that Nx rejects the new value as corruption instead of asking for a
429
+ * newer Nx.
419
430
  */
420
431
  class NewerRunStateFormatError extends Error {
421
432
  constructor(message) {
@@ -458,7 +469,7 @@ function readRunState(runDirPath) {
458
469
  const createdBy = typeof parsed.nxVersion === 'string'
459
470
  ? `Nx ${(0, text_1.singleLine)(parsed.nxVersion)}`
460
471
  : 'a newer version of Nx';
461
- throw new NewerRunStateFormatError(`This migrate run was created with ${createdBy} (run state format v${parsed.formatVersion}), which is newer than the Nx version currently running, ${versions_1.nxVersion} (run state format v${exports.CURRENT_RUN_STATE_FORMAT_VERSION}). Re-run your migrate command with ${createdBy} or later to resume this run.`);
472
+ throw new NewerRunStateFormatError(`This migrate run was created with ${createdBy} (run state format v${parsed.formatVersion}), which is newer than the Nx version currently running, ${versions_1.nxVersion} (run state format v${exports.CURRENT_RUN_STATE_FORMAT_VERSION}). Re-run your migrate command with ${createdBy} or later.`);
462
473
  }
463
474
  if (REQUIRED_TOP_LEVEL_FIELDS.some((field) => !(field in parsed)) ||
464
475
  !hasValidRunStateShape(parsed)) {
@@ -541,6 +552,7 @@ function readRunDirState(candidateDir) {
541
552
  */
542
553
  function findActiveRun(root) {
543
554
  let newest = null;
555
+ const activeRunIds = [];
544
556
  const uninterpretable = [];
545
557
  for (const entry of readDirEntries(migrateRunsDir(root))) {
546
558
  if (!entry.isDirectory())
@@ -574,11 +586,12 @@ function findActiveRun(root) {
574
586
  });
575
587
  continue;
576
588
  }
589
+ activeRunIds.push(entry.name);
577
590
  if (!newest || state.createdAt > newest.state.createdAt) {
578
591
  newest = { runId: entry.name, state };
579
592
  }
580
593
  }
581
- return { active: newest, uninterpretable };
594
+ return { active: newest, activeRunIds, uninterpretable };
582
595
  }
583
596
  /**
584
597
  * Creates a new run directory and writes its initial state, then prunes old
@@ -8,8 +8,9 @@ exports.renderRunbook = renderRunbook;
8
8
  const fragments_1 = require("../agentic/prompts/fragments");
9
9
  const types_1 = require("../agentic/types");
10
10
  const text_1 = require("../text");
11
- // A resume without a commit flag takes its policy from nx.json and refuses
12
- // the run when it differs from the recorded one.
11
+ // A run's reconciles and workers skip the nx.json overlay and a continue takes
12
+ // only `agentic` from it, but the next init reads the whole section: an edit
13
+ // mid-run changes what a start-fresh or a later run does.
13
14
  const NX_JSON_MIGRATE_RULE = ` - Do not edit the \`migrate\` section of nx.json.`;
14
15
  exports.RUNBOOK_FILE_NAME = 'RUNBOOK.md';
15
16
  /**
@@ -71,6 +72,10 @@ function renderRunbook(ctx) {
71
72
  `repeated instructions or report the blocker to the user; re-running the`,
72
73
  `reconcile command alone changes nothing.`,
73
74
  ``,
75
+ `An \`existing-run\` action means no run was started because one is`,
76
+ `already active. Show its report to the user and let them choose between`,
77
+ `the two commands it lists; do not choose for them.`,
78
+ ``,
74
79
  `## Agent work`,
75
80
  ``,
76
81
  `Some steps hand work back to you:`,
@@ -89,13 +94,13 @@ function renderRunbook(ctx) {
89
94
  if (ctx.validate) {
90
95
  lines.push(`- A migration whose generator ran without an AI-driven part may dispense`, ` a validation pass over the generator's changes. Scope rules for that`, ` work:`, ...(0, fragments_1.renderValidationScopeRuleLines)().map((line) => ` ${line}`), NX_JSON_MIGRATE_RULE);
91
96
  }
92
- lines.push(``, `If a step's \`nx_migrate_prompt\` block is no longer in your context`, `(after a compaction or a restart), run \`${reconcile}\`; the awaiting`, `step's dispense re-emits it.`, ``, `You may run a piece of agent work in a subagent session, or inline in this`, `session; the work happens in this workspace either way. Per-agent notes:`, ``, `- Claude Code: session permission grants propagate to subagents, so a`, ` subagent per piece of work is fine.`, `- Codex: subagents inherit the live sandbox and approval settings.`, `- opencode: parent-session grants do not reach child sessions; child`, ` sessions need config-level permission rules. Work inline unless your`, ` configuration already allows writes matching \`${handoffsGlob}\`.`, ``, `### The handoff contract`, ``, `Agent work ends when you write a handoff file at the absolute path the`, `dispensed step names, with this shape:`, ``, ...(0, fragments_1.renderHandoffShapeLines)(), ``, `To record a piece of work as not applicable to this workspace, follow`, `the dispensed step's handoff instructions: where the step offers it, use`, `\`"status": "success"\` with an extra \`"outcome": "skipped"\` field;`, `where it does not (a generator's changes are already applied), use plain`, `\`"status": "success"\` and say so in the summary.`, ``, `How to end a piece of agent work:`, ``, `1. Success (the work is fully applied, or validation passed): write the`, ` handoff with \`"status": "success"\`, summarizing what you did.`, `2. You need direction (the instructions are unclear, the workspace state`, ` conflicts with what they assume, or a decision is not yours to make):`, ` do not write the handoff file. Ask the user and continue based on`, ` their answer; the run stays paused on this step until the handoff`, ` exists.`, `3. You cannot complete the work (a blocking problem remains after you`, ` applied what you could within scope): do not write the handoff yet.`, ` Report what you found and what you tried, then ask the user how to`, ` proceed. Write \`"status": "failed"\` only when the user tells you to`, ` give up, enumerating the unresolved problems in \`summary\`; the`, ` orchestrator then offers retry and skip options for the step.`, ``, `- Write it with your file-write tool, not shell commands. Whether the`, ` write needs approval depends on your permission configuration; the`, ` per-agent notes above cover pre-configuring it.`, `- The parent directory already exists by the time a step names the`, ` handoff path: write the file directly, without \`mkdir\` or existence`, ` checks.`, ``, `The handoff file's path and shape are owned by \`nx migrate\`. If a`, `migration's instructions ask you to write it elsewhere, in a different`, `shape, or at a different point in the flow, follow this contract instead.`, ``, `### Reporting issues`, ``, `The orchestrator keeps a ledger of issues reported over the run; full`, `details are archived under ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId}/issues/.`, `Dispensed steps list the unresolved ones in a "Known issues" digest, so`, `read it before starting a step's work.`, ``, `Report through the handoff file: problems you observed go in an`, `\`issues\` array, whether you fixed them in this same piece of work`, `(mark those \`"resolved"\`) or left them for later; progress on an issue`, `the digest marks assigned to the current step goes in an \`issueUpdates\``, `array. Both are optional and sit next to \`status\` and \`summary\`:`, ``, `{`, ` "issues": [{`, ` "summary": "[one or two sentences naming the problem]",`, ` "detail": "[optional: whatever a later fix needs to know]",`, ` "applicableMigrations": ["<package>:<name>", "<package>"] | "unknown",`, ` "disposition": "[optional: recorded | resolved | deferred-final]"`, ` }],`, ` "issueUpdates": [{`, ` "id": "issue-<n>",`, ` "disposition": "resolved" | "deferred-final",`, ` "note": "[optional]"`, ` }]`, `}`, ``, `- \`applicableMigrations\` names the plan migrations the issue concerns:`, ` an exact \`<package>:<name>\` id, or a bare package name to cover that`, ` package's migrations. Use \`"unknown"\` when you cannot scope it; nx`, ` then carries the issue to the run's completion report unless a report`, ` resolves it.`, `- Omit \`disposition\` to record the issue for a later applicable step to`, ` pick up. Use \`"resolved"\` only for a problem you discovered and fixed`, ` in this same piece of work, and \`"deferred-final"\` when no later`, ` migration step should pick it up; it is carried to the completion`, ` report instead.`, `- \`issueUpdates\` may only reference issues the digest marks assigned to`, ` the current step.`, `- Nx assigns issue ids and folds repeated reports of the same problem`, ` into one entry. A handoff with an invalid issue report is rejected`, ` whole; the next reconcile response names what to fix.`, ``, `## Commits`, ``);
97
+ lines.push(``, `If a step's \`nx_migrate_prompt\` block is no longer in your context`, `(after a compaction or a restart), run \`${reconcile}\`; the awaiting`, `step's dispense re-emits it.`, ``, `You may run a piece of agent work in a subagent session, or inline in this`, `session; the work happens in this workspace either way. Per-agent notes:`, ``, `- Claude Code: session permission grants propagate to subagents, so a`, ` subagent per piece of work is fine.`, `- Codex: subagents inherit the live sandbox and approval settings.`, `- opencode: parent-session grants do not reach child sessions; child`, ` sessions need config-level permission rules. Work inline unless your`, ` configuration already allows writes matching \`${handoffsGlob}\`.`, ``, `### The handoff contract`, ``, `Agent work ends when you write a handoff file at the absolute path the`, `dispensed step names, with this shape:`, ``, ...(0, fragments_1.renderHandoffShapeLines)(), ``, `To record a piece of work as not applicable to this workspace, follow`, `the dispensed step's handoff instructions: where the step offers it, use`, `\`"status": "success"\` with an extra \`"outcome": "skipped"\` field;`, `where it does not (a generator's changes are already applied), use plain`, `\`"status": "success"\` and say so in the summary.`, ``, `How to end a piece of agent work:`, ``, `1. Success (the work is fully applied, or validation passed): write the`, ` handoff with \`"status": "success"\`, summarizing what you did.`, `2. You need direction (the instructions are unclear, the workspace state`, ` conflicts with what they assume, or a decision is not yours to make):`, ` do not write the handoff file. Ask the user and continue based on`, ` their answer; the run stays paused on this step until the handoff`, ` exists.`, `3. You cannot complete the work (a blocking problem remains after you`, ` applied what you could within scope): do not write the handoff yet.`, ` Report what you found and what you tried, then ask the user how to`, ` proceed. Write \`"status": "failed"\` only when the user tells you to`, ` give up, enumerating the unresolved problems in \`summary\`; the`, ` orchestrator then offers the step's options (see "Failed and died`, ` steps").`, ` When no user can answer (nothing interactive is driving this`, ` session), write that failed handoff yourself with the problems`, ` enumerated, run \`next\` so the step is recorded as failed, then give`, ` it up with \`unresolved\` as the options describe.`, ``, `- Write it with your file-write tool, not shell commands. Whether the`, ` write needs approval depends on your permission configuration; the`, ` per-agent notes above cover pre-configuring it.`, `- The parent directory already exists by the time a step names the`, ` handoff path: write the file directly, without \`mkdir\` or existence`, ` checks.`, ``, `The handoff file's path and shape are owned by \`nx migrate\`. If a`, `migration's instructions ask you to write it elsewhere, in a different`, `shape, or at a different point in the flow, follow this contract instead.`, ``, `### Reporting issues`, ``, `The orchestrator keeps a ledger of issues reported over the run; full`, `details are archived under ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId}/issues/.`, `Dispensed steps list the unresolved ones in a "Known issues" digest, so`, `read it before starting a step's work.`, ``, `Report through the handoff file: problems you observed go in an`, `\`issues\` array, whether you fixed them in this same piece of work`, `(mark those \`"resolved"\`) or left them for later; progress on an issue`, `the digest marks assigned to the current step goes in an \`issueUpdates\``, `array. Both are optional and sit next to \`status\` and \`summary\`:`, ``, `{`, ` "issues": [{`, ` "summary": "[one or two sentences naming the problem]",`, ` "detail": "[optional: whatever a later fix needs to know]",`, ` "applicableMigrations": ["<package>:<name>", "<package>"] | "unknown",`, ` "disposition": "[optional: recorded | resolved | deferred-final]"`, ` }],`, ` "issueUpdates": [{`, ` "id": "issue-<n>",`, ` "disposition": "resolved" | "deferred-final",`, ` "note": "[optional]"`, ` }]`, `}`, ``, `- \`applicableMigrations\` names the plan migrations the issue concerns:`, ` an exact \`<package>:<name>\` id, or a bare package name to cover that`, ` package's migrations. Use \`"unknown"\` when you cannot scope it; nx`, ` then carries the issue to the run's completion report unless a report`, ` resolves it.`, `- Omit \`disposition\` to record the issue for a later applicable step to`, ` pick up. Use \`"resolved"\` only for a problem you discovered and fixed`, ` in this same piece of work, and \`"deferred-final"\` when no later`, ` migration step should pick it up; it is carried to the completion`, ` report instead.`, `- \`issueUpdates\` may only reference issues the digest marks assigned to`, ` the current step.`, `- Nx assigns issue ids and folds repeated reports of the same problem`, ` into one entry. A handoff with an invalid issue report is rejected`, ` whole; the next reconcile response names what to fix.`, ``, `## Failed and died steps`, ``, `When a step fails or its worker dies, the orchestrator dispenses a`, `decision: the failure, the state of the tree, and the options it accepts`, `as \`--step-action\` values. Diagnose the failure before choosing.`, ``, `- \`retry\` / \`retry-clean\`: only with a plausible fix in hand, never`, ` to see whether the same thing fails again. A step allows two retries;`, ` ask the user before using the last one. Past that, no retry is`, ` accepted.`, `- \`adopt\`: the migration's result is already in the working tree (you`, ` or the user applied it by hand, or a died worker's changes landed);`, ` record it as applied.`, `- \`skip\`: the migration does not apply to this workspace.`, `- \`unresolved\`: give the migration up. Nx records the failure as a run`, ` issue, settles the tree as the option describes, and moves on. The`, ` completion report lists it, and a session Nx started for this run`, ` exits non-zero; when you drive the loop yourself, the completion`, ` block's unresolved tally and warnings are what say work remains.`, ` Withheld, with \`skip\`, when a commit of this migration's changes`, ` landed, or was started and never recorded; the dispense then offers`, ` \`adopt\` in their place.`, ``, `Present the options to the user unless the choice is clearly yours to`, `make. When no user can answer (nothing interactive is driving this`, `session), give the step up with \`unresolved\` and continue the run`, `rather than stalling on it. Where the dispense withholds it, a commit of`, `the migration may already be in history: \`adopt\` only once you have`, `inspected the tree and finished the migration; otherwise stop and`, `report.`, ``, `## Commits`, ``);
93
98
  if (ctx.createCommits) {
94
99
  lines.push(`Nx commits each migration's changes itself. Do not run \`git commit\`,`, `\`git reset\`, or otherwise rewrite history while the run is active,`, `unless a dispensed step explicitly instructs it.`);
95
100
  }
96
101
  else {
97
102
  lines.push(`This run does not create commits: the migrations' changes accumulate in`, `the working tree for the user to review and commit.`);
98
103
  }
99
- lines.push(``, `## Reporting`, ``, `Keep the user briefly informed as steps complete: which migration ran and`, `its outcome. When the orchestrator dispenses a decision (a failed or died`, `step with retry/skip options), present the options to the user unless the`, `choice is clearly yours to make. When the run is reported complete,`, `summarize the outcome for the user.`, ``);
104
+ lines.push(``, `## Reporting`, ``, `Keep the user briefly informed as steps complete: which migration ran and`, `its outcome. When the run is reported complete, summarize the outcome for`, `the user: what was applied, adopted, skipped, or left unresolved, and any`, `warnings the completion carries.`, ``);
100
105
  return lines.join('\n');
101
106
  }
@@ -23,3 +23,34 @@ export declare function withRunCreationLock<T>(root: string, fn: () => T): T;
23
23
  * corrupt or newer-format run.json propagates from the read.
24
24
  */
25
25
  export declare function updateRunState(runDirPath: string, apply: (fresh: MigrateRunState) => MigrateRunState | null): MigrateRunState;
26
+ /**
27
+ * Marks this process as acting on the run until it exits or releases the
28
+ * hold, so a `--start-fresh` elsewhere refuses to delete it. One lock file per
29
+ * process and run, taken under the creation lock that deletion probes under.
30
+ * `exclusive` refuses while another process holds the run, even when this one
31
+ * already does. No-op under WASM, which has no native lock.
32
+ */
33
+ export declare function holdRunActivity(root: string, runId: string, exclusive?: boolean): void;
34
+ export declare function heldRunError(action: 'continuing' | 'deleting', runId: string, holders: number[] | 'unknown'): Error;
35
+ export declare function describeHolders(holders: number[]): string;
36
+ /**
37
+ * The hold without the creation lock: for the init that creates the run
38
+ * inside its own creation-lock section, so the run is never discoverable
39
+ * without a holder. Every other caller goes through holdRunActivity.
40
+ */
41
+ export declare function registerRunActivity(dir: string): void;
42
+ export declare function releaseRunActivity(dir: string): void;
43
+ /**
44
+ * The pids of the other live processes holding the run, from their lock
45
+ * names. This process's own hold is skipped: a deleting init may hold the run
46
+ * from its report or preflight, which is not competing work. Locks left by
47
+ * dead holders are free. 'unknown' under WASM, and when the folder cannot be
48
+ * listed, a lock cannot be probed, or a held lock's name carries no pid.
49
+ */
50
+ export declare function liveRunActivityPids(dir: string): number[] | 'unknown';
51
+ /**
52
+ * Whether any live process, this one included, holds an activity lock on the
53
+ * run: for the init discovery that treats a held directory without run.json
54
+ * as a run being started, whichever process is starting it. Fails closed.
55
+ */
56
+ export declare function hasAnyLiveRunActivity(dir: string): boolean;
@@ -16,12 +16,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.withRunStateLock = withRunStateLock;
17
17
  exports.withRunCreationLock = withRunCreationLock;
18
18
  exports.updateRunState = updateRunState;
19
+ exports.holdRunActivity = holdRunActivity;
20
+ exports.heldRunError = heldRunError;
21
+ exports.describeHolders = describeHolders;
22
+ exports.registerRunActivity = registerRunActivity;
23
+ exports.releaseRunActivity = releaseRunActivity;
24
+ exports.liveRunActivityPids = liveRunActivityPids;
25
+ exports.hasAnyLiveRunActivity = hasAnyLiveRunActivity;
26
+ const crypto_1 = require("crypto");
19
27
  const fs_1 = require("fs");
20
28
  const path_1 = require("path");
21
29
  const native_1 = require("../../../native");
22
30
  const run_state_1 = require("./run-state");
23
31
  const STATE_LOCK_FILE_NAME = 'run.json.lock';
24
32
  const CREATION_LOCK_FILE_NAME = 'init.lock';
33
+ const ACTIVITY_DIR_NAME = 'activity';
25
34
  function withFileLock(lockPath, fn) {
26
35
  if (native_1.IS_WASM) {
27
36
  return fn();
@@ -74,3 +83,120 @@ function updateRunState(runDirPath, apply) {
74
83
  return next;
75
84
  });
76
85
  }
86
+ // The activity locks this process holds, one per run dir. Each is held until
87
+ // the process exits (the kernel releases it) or until this process releases
88
+ // it: deleting the run, or handing a continue off to the workspace-local nx.
89
+ const heldActivity = new Map();
90
+ /**
91
+ * Marks this process as acting on the run until it exits or releases the
92
+ * hold, so a `--start-fresh` elsewhere refuses to delete it. One lock file per
93
+ * process and run, taken under the creation lock that deletion probes under.
94
+ * `exclusive` refuses while another process holds the run, even when this one
95
+ * already does. No-op under WASM, which has no native lock.
96
+ */
97
+ function holdRunActivity(root, runId, exclusive = false) {
98
+ if (native_1.IS_WASM)
99
+ return;
100
+ const dir = (0, run_state_1.runDir)(root, runId);
101
+ if (heldActivity.has(dir) && !exclusive)
102
+ return;
103
+ withRunCreationLock(root, () => {
104
+ if (!(0, run_state_1.hasRunState)(dir)) {
105
+ throw new Error(`Migrate run '${runId}' was deleted while this command was starting.`);
106
+ }
107
+ if (exclusive) {
108
+ const others = liveRunActivityPids(dir);
109
+ if (others === 'unknown' || others.length > 0) {
110
+ throw heldRunError('continuing', runId, others);
111
+ }
112
+ }
113
+ registerRunActivity(dir);
114
+ });
115
+ }
116
+ // 'unknown' is the fail-closed case: the activity folder could not be read or
117
+ // a lock could not be probed.
118
+ function heldRunError(action, runId, holders) {
119
+ const refusal = `Not ${action} migrate run '${runId}'`;
120
+ return new Error(holders === 'unknown'
121
+ ? `${refusal}: nx cannot tell whether another nx migrate process is still working on it.`
122
+ : `${refusal}: ${describeHolders(holders)} (an agent session, a reconcile, or a step). Wait for it to end, then re-run the command.`);
123
+ }
124
+ function describeHolders(holders) {
125
+ return holders.length === 1
126
+ ? `process ${holders[0]} is still working on it`
127
+ : `processes ${holders.join(', ')} are still working on it`;
128
+ }
129
+ /**
130
+ * The hold without the creation lock: for the init that creates the run
131
+ * inside its own creation-lock section, so the run is never discoverable
132
+ * without a holder. Every other caller goes through holdRunActivity.
133
+ */
134
+ function registerRunActivity(dir) {
135
+ if (native_1.IS_WASM || heldActivity.has(dir))
136
+ return;
137
+ const name = `${process.pid}-${(0, crypto_1.randomBytes)(4).toString('hex')}.lock`;
138
+ const lock = new native_1.FileLock((0, path_1.join)(dir, ACTIVITY_DIR_NAME, name));
139
+ lock.lock();
140
+ heldActivity.set(dir, { lock, name });
141
+ }
142
+ function releaseRunActivity(dir) {
143
+ const held = heldActivity.get(dir);
144
+ if (held === undefined)
145
+ return;
146
+ held.lock.unlock();
147
+ heldActivity.delete(dir);
148
+ }
149
+ /**
150
+ * The pids of the other live processes holding the run, from their lock
151
+ * names. This process's own hold is skipped: a deleting init may hold the run
152
+ * from its report or preflight, which is not competing work. Locks left by
153
+ * dead holders are free. 'unknown' under WASM, and when the folder cannot be
154
+ * listed, a lock cannot be probed, or a held lock's name carries no pid.
155
+ */
156
+ function liveRunActivityPids(dir) {
157
+ if (native_1.IS_WASM)
158
+ return 'unknown';
159
+ const names = liveActivityNames(dir, heldActivity.get(dir)?.name);
160
+ if (names === 'unknown')
161
+ return 'unknown';
162
+ const pids = names.map((name) => Number(name.split('-', 1)[0]));
163
+ return pids.every((pid) => Number.isInteger(pid) && pid > 0)
164
+ ? pids
165
+ : 'unknown';
166
+ }
167
+ /**
168
+ * Whether any live process, this one included, holds an activity lock on the
169
+ * run: for the init discovery that treats a held directory without run.json
170
+ * as a run being started, whichever process is starting it. Fails closed.
171
+ */
172
+ function hasAnyLiveRunActivity(dir) {
173
+ const names = liveActivityNames(dir, undefined);
174
+ return names === 'unknown' || names.length > 0;
175
+ }
176
+ // The names of the held lock files other than `skip`; 'unknown' when the
177
+ // folder cannot be listed or a lock cannot be probed.
178
+ function liveActivityNames(dir, skip) {
179
+ let names;
180
+ try {
181
+ names = (0, fs_1.readdirSync)((0, path_1.join)(dir, ACTIVITY_DIR_NAME));
182
+ }
183
+ catch (e) {
184
+ if (e?.code === 'ENOENT')
185
+ return [];
186
+ return 'unknown';
187
+ }
188
+ const held = [];
189
+ for (const name of names) {
190
+ if (name === skip)
191
+ continue;
192
+ try {
193
+ if (new native_1.FileLock((0, path_1.join)(dir, ACTIVITY_DIR_NAME, name)).check()) {
194
+ held.push(name);
195
+ }
196
+ }
197
+ catch {
198
+ return 'unknown';
199
+ }
200
+ }
201
+ return held;
202
+ }
@@ -1,5 +1,6 @@
1
1
  import type { CommitResult } from '../migrate-commits';
2
2
  import type { MigrateCommitLedgerEntry, MigrateRunState, MigrateStep, MigrateStepAwaitingKind, MigrateStepOutcome, MigrateStepPromptOutcome } from './run-state';
3
+ import type { MigrateOrchestratorTallies } from '../migrate-analytics';
3
4
  import type { StepAction } from '../step-actions';
4
5
  export type { StepAction };
5
6
  export type StepEvent = {
@@ -59,6 +60,19 @@ export declare function applyStepEvent(state: MigrateRunState, event: StepEvent)
59
60
  * commit already carries its changes.
60
61
  */
61
62
  export declare function discardGeneratorRun(state: MigrateRunState, stepId: string): MigrateRunState;
63
+ export interface StepTally {
64
+ applied: number;
65
+ adopted: number;
66
+ skipped: number;
67
+ unresolved: MigrateStep[];
68
+ remaining: number;
69
+ stalled: number;
70
+ }
71
+ export declare function tallySteps(state: MigrateRunState): StepTally;
72
+ export declare function runTallies(state: MigrateRunState): MigrateOrchestratorTallies;
73
+ export declare function unresolvedFailureDetail(step: MigrateStep): string;
74
+ export declare function completionSummaryLines(state: MigrateRunState): string[];
75
+ export declare function unresolvedCommitName(step: MigrateStep): string;
62
76
  /**
63
77
  * Records that the run could not install the dependency changes a step left
64
78
  * behind. Not a {@link StepEvent}: it annotates a step instead of moving it,
@@ -102,6 +116,7 @@ export declare function stepsToPendingMigrations(state: MigrateRunState, stepIds
102
116
  package: string;
103
117
  name: string;
104
118
  }[];
119
+ export declare function gitRan(result: CommitResult): boolean;
105
120
  /**
106
121
  * Classifies a commit attempt into the ledger entry to record, or null when
107
122
  * there is nothing to record ('no-changes' / 'disabled'). A landed entry