nx 23.2.1 → 23.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/src/command-line/format/format.js +2 -18
  2. package/dist/src/command-line/graph/graph.js +42 -3
  3. package/dist/src/command-line/init/implementation/format.js +2 -3
  4. package/dist/src/command-line/migrate/agentic/definitions.js +75 -19
  5. package/dist/src/command-line/migrate/agentic/format-command.d.ts +14 -0
  6. package/dist/src/command-line/migrate/agentic/format-command.js +44 -0
  7. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +3 -3
  8. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +3 -3
  9. package/dist/src/command-line/migrate/agentic/handoff.d.ts +27 -17
  10. package/dist/src/command-line/migrate/agentic/handoff.js +107 -24
  11. package/dist/src/command-line/migrate/agentic/instruction-files.d.ts +21 -0
  12. package/dist/src/command-line/migrate/agentic/instruction-files.js +36 -0
  13. package/dist/src/command-line/migrate/agentic/prompts/fragments.d.ts +16 -0
  14. package/dist/src/command-line/migrate/agentic/prompts/fragments.js +70 -0
  15. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.js +4 -3
  16. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.d.ts +25 -1
  17. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.js +57 -34
  18. package/dist/src/command-line/migrate/agentic/run-step.js +28 -4
  19. package/dist/src/command-line/migrate/agentic/runner.d.ts +17 -8
  20. package/dist/src/command-line/migrate/agentic/runner.js +110 -55
  21. package/dist/src/command-line/migrate/agentic/types.d.ts +23 -8
  22. package/dist/src/command-line/migrate/agentic/types.js +10 -7
  23. package/dist/src/command-line/migrate/migrate-commits.d.ts +1 -0
  24. package/dist/src/command-line/migrate/migrate-commits.js +9 -0
  25. package/dist/src/command-line/migrate/migrate-output.d.ts +3 -7
  26. package/dist/src/command-line/migrate/migrate-output.js +3 -7
  27. package/dist/src/command-line/migrate/migrate.js +21 -8
  28. package/dist/src/command-line/migrate/run/agent-output.d.ts +10 -0
  29. package/dist/src/command-line/migrate/run/agent-output.js +16 -2
  30. package/dist/src/command-line/migrate/run/agent-work-payload.d.ts +37 -0
  31. package/dist/src/command-line/migrate/run/agent-work-payload.js +142 -0
  32. package/dist/src/command-line/migrate/run/atomic-write.d.ts +8 -0
  33. package/dist/src/command-line/migrate/run/atomic-write.js +17 -0
  34. package/dist/src/command-line/migrate/run/index.d.ts +1 -1
  35. package/dist/src/command-line/migrate/run/issues.d.ts +106 -0
  36. package/dist/src/command-line/migrate/run/issues.js +980 -0
  37. package/dist/src/command-line/migrate/run/orchestrator.d.ts +1 -0
  38. package/dist/src/command-line/migrate/run/orchestrator.js +662 -105
  39. package/dist/src/command-line/migrate/run/run-state.d.ts +43 -8
  40. package/dist/src/command-line/migrate/run/run-state.js +184 -20
  41. package/dist/src/command-line/migrate/run/runbook.d.ts +17 -0
  42. package/dist/src/command-line/migrate/run/runbook.js +97 -0
  43. package/dist/src/command-line/migrate/run/state-lock.d.ts +2 -2
  44. package/dist/src/command-line/migrate/run/state-lock.js +2 -2
  45. package/dist/src/command-line/migrate/run/state-machine.d.ts +5 -1
  46. package/dist/src/command-line/migrate/run/state-machine.js +27 -1
  47. package/dist/src/command-line/migrate/run/worker.js +206 -56
  48. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +5 -1
  49. package/dist/src/command-line/release/publish.js +11 -4
  50. package/dist/src/command-line/release/utils/remote-release-clients/github.js +3 -3
  51. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +3 -3
  52. package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.d.ts +2 -0
  53. package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.js +35 -0
  54. package/dist/src/command-line/yargs-utils/shared-options.d.ts +1 -1
  55. package/dist/src/command-line/yargs-utils/shared-options.js +22 -4
  56. package/dist/src/config/workspace-json-project-json.d.ts +2 -0
  57. package/dist/src/core/graph/main.js +1 -1
  58. package/dist/src/daemon/client/client.d.ts +4 -2
  59. package/dist/src/daemon/client/client.js +20 -11
  60. package/dist/src/daemon/message-types/run-tasks-execution-hooks.d.ts +3 -2
  61. package/dist/src/daemon/server/handle-hash-tasks.d.ts +8 -2
  62. package/dist/src/daemon/server/handle-hash-tasks.js +16 -3
  63. package/dist/src/daemon/server/handle-outputs-changes.d.ts +2 -2
  64. package/dist/src/daemon/server/handle-outputs-changes.js +21 -6
  65. package/dist/src/daemon/server/handle-tasks-execution-hooks.d.ts +3 -2
  66. package/dist/src/daemon/server/outputs-tracking.d.ts +6 -0
  67. package/dist/src/daemon/server/outputs-tracking.js +18 -0
  68. package/dist/src/daemon/server/project-graph-incremental-recomputation.d.ts +22 -7
  69. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +99 -69
  70. package/dist/src/daemon/server/restart-checks.d.ts +22 -0
  71. package/dist/src/daemon/server/restart-checks.js +77 -0
  72. package/dist/src/daemon/server/server.js +28 -18
  73. package/dist/src/daemon/server/shutdown-utils.d.ts +0 -5
  74. package/dist/src/daemon/server/shutdown-utils.js +3 -26
  75. package/dist/src/hasher/hash-task.js +20 -16
  76. package/dist/src/hasher/native-task-hasher-impl.d.ts +8 -0
  77. package/dist/src/hasher/native-task-hasher-impl.js +46 -3
  78. package/dist/src/hasher/task-hasher.d.ts +9 -0
  79. package/dist/src/hasher/task-hasher.js +14 -0
  80. package/dist/src/native/index.d.ts +202 -21
  81. package/dist/src/native/index.js +3 -3
  82. package/dist/src/native/native-bindings.js +3 -1
  83. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  84. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  85. package/dist/src/plugins/js/lock-file/pnpm-parser.js +1 -1
  86. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.d.ts +2 -0
  87. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.js +22 -0
  88. package/dist/src/plugins/js/utils/register.js +2 -1
  89. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +2 -1
  90. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +3 -2
  91. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +5 -1
  92. package/dist/src/project-graph/plugins/loaded-nx-plugin.d.ts +3 -2
  93. package/dist/src/project-graph/plugins/loaded-nx-plugin.js +5 -1
  94. package/dist/src/project-graph/plugins/task-results-stub.d.ts +50 -0
  95. package/dist/src/project-graph/plugins/task-results-stub.js +111 -0
  96. package/dist/src/project-graph/plugins/tasks-execution-hooks.d.ts +3 -2
  97. package/dist/src/project-graph/plugins/tasks-execution-hooks.js +2 -1
  98. package/dist/src/project-graph/project-graph.js +2 -0
  99. package/dist/src/tasks-runner/batch/run-batch.js +15 -0
  100. package/dist/src/tasks-runner/cache.d.ts +35 -0
  101. package/dist/src/tasks-runner/cache.js +40 -0
  102. package/dist/src/tasks-runner/create-task-graph.js +1 -0
  103. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  104. package/dist/src/tasks-runner/forked-process-task-runner.d.ts +1 -1
  105. package/dist/src/tasks-runner/forked-process-task-runner.js +8 -7
  106. package/dist/src/tasks-runner/init-tasks-runner.js +5 -1
  107. package/dist/src/tasks-runner/is-tui-enabled.js +4 -3
  108. package/dist/src/tasks-runner/life-cycle.d.ts +7 -0
  109. package/dist/src/tasks-runner/life-cycle.js +7 -0
  110. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.d.ts +35 -0
  111. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.js +146 -0
  112. package/dist/src/tasks-runner/prune-task-graph.d.ts +17 -0
  113. package/dist/src/tasks-runner/prune-task-graph.js +45 -0
  114. package/dist/src/tasks-runner/run-command.js +14 -3
  115. package/dist/src/tasks-runner/running-tasks/batch-process.d.ts +93 -19
  116. package/dist/src/tasks-runner/running-tasks/batch-process.js +184 -88
  117. package/dist/src/tasks-runner/task-orchestrator.d.ts +64 -2
  118. package/dist/src/tasks-runner/task-orchestrator.js +232 -46
  119. package/dist/src/tasks-runner/terminal-output-path.d.ts +11 -0
  120. package/dist/src/tasks-runner/terminal-output-path.js +18 -0
  121. package/dist/src/tasks-runner/utils.js +5 -2
  122. package/dist/src/utils/ab-testing.d.ts +3 -0
  123. package/dist/src/utils/ab-testing.js +11 -3
  124. package/dist/src/utils/chunkify.d.ts +1 -7
  125. package/dist/src/utils/chunkify.js +2 -8
  126. package/dist/src/utils/command-line-utils.d.ts +19 -0
  127. package/dist/src/utils/command-line-utils.js +10 -3
  128. package/dist/src/utils/formatters/index.d.ts +6 -0
  129. package/dist/src/utils/formatters/index.js +5 -0
  130. package/dist/src/utils/formatters/prettier.d.ts +0 -8
  131. package/dist/src/utils/formatters/prettier.js +4 -26
  132. package/dist/src/utils/installed-nx-version.js +2 -48
  133. package/dist/src/utils/min-release-age/packument.js +1 -1
  134. package/dist/src/utils/output.d.ts +22 -3
  135. package/dist/src/utils/output.js +28 -5
  136. package/dist/src/utils/package-json.d.ts +25 -0
  137. package/dist/src/utils/package-json.js +41 -6
  138. package/dist/src/utils/package-manager.d.ts +6 -0
  139. package/dist/src/utils/package-manager.js +10 -0
  140. package/dist/src/utils/provenance.js +20 -4
  141. package/dist/src/utils/workspace-context.d.ts +47 -3
  142. package/dist/src/utils/workspace-context.js +162 -10
  143. package/package.json +12 -12
  144. package/schemas/nx-schema.json +4 -0
  145. package/schemas/project-schema.json +4 -0
  146. package/dist/src/daemon/server/file-watching/route-workspace-changes.d.ts +0 -9
  147. package/dist/src/daemon/server/file-watching/route-workspace-changes.js +0 -76
  148. package/dist/src/daemon/server/watcher.d.ts +0 -18
  149. package/dist/src/daemon/server/watcher.js +0 -126
@@ -9,6 +9,7 @@ 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 declare const ISSUE_ID: RegExp;
12
13
  declare const MIGRATE_RUN_STATUSES: readonly ['active', 'completed'];
13
14
  export type MigrateRunStatus = (typeof MIGRATE_RUN_STATUSES)[number];
14
15
  export interface MigrateRunRound {
@@ -18,8 +19,11 @@ export interface MigrateRunRound {
18
19
  }
19
20
  declare const MIGRATE_STEP_STATUSES: readonly ['pending', 'dispensed', 'running', 'awaiting-prompt-outcome', 'succeeded', 'failed', 'skipped', 'died'];
20
21
  export type MigrateStepStatus = (typeof MIGRATE_STEP_STATUSES)[number];
22
+ export declare const TERMINAL_STEP_STATUSES: ReadonlySet<MigrateStepStatus>;
21
23
  declare const PROMPT_OUTCOME_STATUSES: readonly ['completed', 'skipped', 'failed'];
22
24
  export type PromptOutcomeStatus = (typeof PROMPT_OUTCOME_STATUSES)[number];
25
+ declare const MIGRATE_STEP_AWAITING_KINDS: readonly ['migration-prompt', 'generator-validation'];
26
+ export type MigrateStepAwaitingKind = (typeof MIGRATE_STEP_AWAITING_KINDS)[number];
23
27
  export interface MigrateStepOutcome {
24
28
  fileChanges?: string[];
25
29
  gitRefAfter?: string;
@@ -46,7 +50,12 @@ export interface MigrateStep {
46
50
  depsHashAtDispense?: string;
47
51
  outcome?: MigrateStepOutcome;
48
52
  promptOutcome?: MigrateStepPromptOutcome;
53
+ awaitingKind?: MigrateStepAwaitingKind;
49
54
  generatorCompleted?: boolean;
55
+ generatorCompletedAtAttempt?: number;
56
+ agenticWaived?: boolean;
57
+ validationOwed?: boolean;
58
+ generatorMadeChanges?: boolean;
50
59
  installFailed?: boolean;
51
60
  }
52
61
  declare const MIGRATE_COMMIT_KINDS: readonly ['checkpoint', 'landed', 'failed'];
@@ -55,11 +64,37 @@ export interface MigrateCommitLedgerEntry {
55
64
  sha?: string;
56
65
  kind: MigrateCommitKind;
57
66
  stepIds: string[];
67
+ issueIds?: string[];
68
+ }
69
+ /**
70
+ * Dedup key: the same underlying problem reported twice folds into one ledger
71
+ * entry. Derived from the normalized summary, so agent text never becomes a
72
+ * path or command fragment; the state reader re-checks the derivation on
73
+ * every persisted entry.
74
+ */
75
+ export declare function issueFingerprint(summary: string): string;
76
+ export declare const MIGRATE_ISSUE_DISPOSITIONS: readonly ['recorded', 'resolved', 'deferred-final'];
77
+ export type MigrateIssueDisposition = (typeof MIGRATE_ISSUE_DISPOSITIONS)[number];
78
+ export interface MigrateRunIssue {
79
+ id: string;
80
+ fingerprint: string;
81
+ summary: string;
82
+ reportedByStepId: string;
83
+ applicableStepIds: string[] | 'unknown';
84
+ disposition: MigrateIssueDisposition;
85
+ claimedByStepId?: string;
86
+ resolvedByStepId?: string;
87
+ resolvedAtCommitCount?: number;
58
88
  }
59
89
  export interface MigrateRunAnalytics {
60
90
  startEmitted: boolean;
61
91
  completeEmitted: boolean;
62
92
  }
93
+ export interface MigrateRunNoProgress {
94
+ fingerprint: string;
95
+ consecutiveCount: number;
96
+ firstSeenAt: string;
97
+ }
63
98
  export interface MigrateRunState {
64
99
  formatVersion: number;
65
100
  runId: string;
@@ -69,9 +104,13 @@ export interface MigrateRunState {
69
104
  createCommits: boolean;
70
105
  commitPrefix: string;
71
106
  skipInstall?: boolean;
107
+ validate?: boolean;
108
+ runbookPath?: string;
72
109
  rounds: MigrateRunRound[];
73
110
  steps: MigrateStep[];
74
111
  commits: MigrateCommitLedgerEntry[];
112
+ issues?: MigrateRunIssue[];
113
+ noProgress?: MigrateRunNoProgress;
75
114
  checkpointFailed?: boolean;
76
115
  analytics: MigrateRunAnalytics;
77
116
  }
@@ -85,7 +124,7 @@ export declare function runHandoffsDir(runDirPath: string): string;
85
124
  * ignoring a newer active run would start a competing run on top of it.
86
125
  *
87
126
  * Adding a member to any persisted closed set (run status, step status,
88
- * prompt-outcome status, commit kind) needs a
127
+ * awaiting kind, prompt-outcome status, commit kind, issue disposition) needs a
89
128
  * `CURRENT_RUN_STATE_FORMAT_VERSION` bump: without it, an older Nx reading
90
129
  * the new value would reject the run as corrupt (the closed-set validation
91
130
  * fails) instead of refusing with this error's ask for a newer Nx.
@@ -94,13 +133,9 @@ export declare class NewerRunStateFormatError extends Error {
94
133
  constructor(message: string);
95
134
  }
96
135
  /**
97
- * Reads and validates `run.json` from a run directory.
98
- *
99
- * A `formatVersion` newer than {@link CURRENT_RUN_STATE_FORMAT_VERSION} means
100
- * the run was created by a newer Nx than the one currently running, so the
101
- * shape may not be interpretable here; this throws rather than attempting a
102
- * best-effort read. An older `formatVersion` is returned as-is: only v1
103
- * exists today, so there is nothing to migrate yet.
136
+ * Reads and validates `run.json` from a run directory. A `formatVersion`
137
+ * newer than {@link CURRENT_RUN_STATE_FORMAT_VERSION} throws instead of being
138
+ * read best-effort; an older one is returned as-is.
104
139
  */
105
140
  export declare function readRunState(runDirPath: string): MigrateRunState;
106
141
  /**
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NewerRunStateFormatError = exports.SHELL_SAFE_VALUE = exports.RUN_STATE_FILE_NAME = exports.CURRENT_RUN_STATE_FORMAT_VERSION = void 0;
3
+ exports.NewerRunStateFormatError = 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.issueFingerprint = issueFingerprint;
4
5
  exports.migrateRunsDir = migrateRunsDir;
5
6
  exports.runDir = runDir;
6
7
  exports.runHandoffsDir = runHandoffsDir;
@@ -13,6 +14,7 @@ const fs_1 = require("fs");
13
14
  const crypto_1 = require("crypto");
14
15
  const path_1 = require("path");
15
16
  const fileutils_1 = require("../../../utils/fileutils");
17
+ const atomic_write_1 = require("./atomic-write");
16
18
  const git_utils_1 = require("../../../utils/git-utils");
17
19
  const versions_1 = require("../../../utils/versions");
18
20
  const types_1 = require("../agentic/types");
@@ -48,7 +50,21 @@ const PLAN_SNAPSHOT_NAME = /^plan-\d+\.json$/;
48
50
  * transition with, and the worker throws that reason, which puts it in front
49
51
  * of the agent without passing the block-safe writer.
50
52
  */
51
- const STEP_ID = /^step-\d+$/;
53
+ const STEP_ID = /^step-\d{1,18}$/;
54
+ // The id names the archived detail file in the run's issues directory, so a
55
+ // tampered value must not resolve outside it. The suffix is bounded so the
56
+ // allocator, which mints against this same pattern, can never produce an id
57
+ // past the filesystem's component limit and then fail forever.
58
+ exports.ISSUE_ID = /^issue-\d{1,18}$/;
59
+ /**
60
+ * A resume joins this to the run directory and re-emits the file's bytes
61
+ * verbatim, so a tampered value naming a sibling (`run.json`, a plan
62
+ * snapshot) would leak that file's bytes past every line-safety check.
63
+ */
64
+ const RUNBOOK_NAME = /^RUNBOOK\.md$/;
65
+ // The terminators `singleLine` collapses. An embedded one could open a forged
66
+ // block at a line start in the stdout the agent scans.
67
+ const LINE_TERMINATORS = /[\r\n\u000b\u000c\u0085\u2028\u2029]/;
52
68
  // Keeps `.nx/migrate-runs` from growing unbounded across many `nx migrate`
53
69
  // invocations over the life of a workspace.
54
70
  const MAX_RETAINED_COMPLETED_RUNS = 5;
@@ -66,8 +82,35 @@ const MIGRATE_STEP_STATUSES = [
66
82
  'skipped',
67
83
  'died',
68
84
  ];
85
+ // 'failed' and 'died' are not terminal: both can be re-armed into a fresh
86
+ // attempt.
87
+ exports.TERMINAL_STEP_STATUSES = new Set([
88
+ 'succeeded',
89
+ 'skipped',
90
+ ]);
69
91
  const PROMPT_OUTCOME_STATUSES = ['completed', 'skipped', 'failed'];
92
+ const MIGRATE_STEP_AWAITING_KINDS = [
93
+ 'migration-prompt',
94
+ 'generator-validation',
95
+ ];
70
96
  const MIGRATE_COMMIT_KINDS = ['checkpoint', 'landed', 'failed'];
97
+ /**
98
+ * Dedup key: the same underlying problem reported twice folds into one ledger
99
+ * entry. Derived from the normalized summary, so agent text never becomes a
100
+ * path or command fragment; the state reader re-checks the derivation on
101
+ * every persisted entry.
102
+ */
103
+ function issueFingerprint(summary) {
104
+ return (0, crypto_1.createHash)('sha256')
105
+ .update(summary.toLowerCase().replace(/\s+/g, ' ').trim())
106
+ .digest('hex')
107
+ .slice(0, 16);
108
+ }
109
+ exports.MIGRATE_ISSUE_DISPOSITIONS = [
110
+ 'recorded',
111
+ 'resolved',
112
+ 'deferred-final',
113
+ ];
71
114
  const REQUIRED_TOP_LEVEL_FIELDS = [
72
115
  'formatVersion',
73
116
  'runId',
@@ -127,6 +170,16 @@ function isOptionalStringArray(value) {
127
170
  return (value === undefined ||
128
171
  (Array.isArray(value) && value.every((item) => typeof item === 'string')));
129
172
  }
173
+ function isOptionalMatching(pattern, value) {
174
+ return (value === undefined || (typeof value === 'string' && pattern.test(value)));
175
+ }
176
+ function isLineSafeString(value) {
177
+ return typeof value === 'string' && !LINE_TERMINATORS.test(value);
178
+ }
179
+ function isStepIdArray(value) {
180
+ return (Array.isArray(value) &&
181
+ value.every((id) => typeof id === 'string' && STEP_ID.test(id)));
182
+ }
130
183
  function isRoundShape(value) {
131
184
  return (isPlainObject(value) &&
132
185
  typeof value.index === 'number' &&
@@ -156,7 +209,11 @@ function isStepShape(value) {
156
209
  typeof value.migrationId === 'string' &&
157
210
  exports.SHELL_SAFE_VALUE.test(value.migrationId) &&
158
211
  isOneOf(MIGRATE_STEP_STATUSES, value.status) &&
159
- typeof value.attempt === 'number' &&
212
+ // The attempt is interpolated into the stored-payload file name and
213
+ // range-compared against it (agent-work-payload.ts), so a fractional or
214
+ // non-finite value (JSON's 1e400 parses to Infinity) must be refused here.
215
+ Number.isSafeInteger(value.attempt) &&
216
+ value.attempt >= 1 &&
160
217
  typeof value.dispenseCount === 'number' &&
161
218
  isOptionalBoolean(value.hasGenerator) &&
162
219
  isOptionalNumber(value.pid) &&
@@ -167,7 +224,19 @@ function isStepShape(value) {
167
224
  isOptionalString(value.depsHashAtDispense) &&
168
225
  isStepOutcomeShape(value.outcome) &&
169
226
  isPromptOutcomeShape(value.promptOutcome) &&
227
+ (value.awaitingKind === undefined ||
228
+ isOneOf(MIGRATE_STEP_AWAITING_KINDS, value.awaitingKind)) &&
170
229
  isOptionalBoolean(value.generatorCompleted) &&
230
+ // Bounded by the step's attempt: a higher value could not name one that
231
+ // exists.
232
+ (value.generatorCompletedAtAttempt === undefined ||
233
+ (Number.isSafeInteger(value.generatorCompletedAtAttempt) &&
234
+ value.generatorCompletedAtAttempt >= 1 &&
235
+ value.generatorCompletedAtAttempt <=
236
+ value.attempt)) &&
237
+ isOptionalBoolean(value.agenticWaived) &&
238
+ isOptionalBoolean(value.validationOwed) &&
239
+ isOptionalBoolean(value.generatorMadeChanges) &&
171
240
  isOptionalBoolean(value.installFailed) &&
172
241
  // A cross-field invariant the rest of the loop relies on: a running step
173
242
  // without a pid is never reclassified as died and no step action targets
@@ -177,9 +246,97 @@ function isStepShape(value) {
177
246
  function isCommitLedgerEntryShape(value) {
178
247
  return (isPlainObject(value) &&
179
248
  isOneOf(MIGRATE_COMMIT_KINDS, value.kind) &&
180
- Array.isArray(value.stepIds) &&
181
- value.stepIds.every((id) => typeof id === 'string' && STEP_ID.test(id)) &&
182
- isOptionalSha(value.sha));
249
+ isStepIdArray(value.stepIds) &&
250
+ isOptionalSha(value.sha) &&
251
+ (value.issueIds === undefined ||
252
+ (Array.isArray(value.issueIds) &&
253
+ value.issueIds.every((id) => typeof id === 'string' && exports.ISSUE_ID.test(id)))));
254
+ }
255
+ // The id names the archived `issues/<id>.json`, so a duplicate would alias two
256
+ // entries onto one file. Runs after isIssueShape, so the ids are known strings.
257
+ function hasUniqueIssueIds(issues) {
258
+ return (new Set(issues.map((issue) => issue.id)).size ===
259
+ issues.length);
260
+ }
261
+ // A duplicate would make every step lookup ambiguous, and a bare-package
262
+ // report against it would write applicability this same reader rejects.
263
+ function hasUniqueStepIds(steps) {
264
+ return (new Set(steps.map((step) => step.id)).size ===
265
+ steps.length);
266
+ }
267
+ // The fingerprint is the duplicate-report fold key; two entries sharing one
268
+ // would make ledger order decide which entry a report folds into.
269
+ function hasUniqueIssueFingerprints(issues) {
270
+ return (new Set(issues.map((issue) => issue.fingerprint)).size === issues.length);
271
+ }
272
+ // Cross-entry references, checked after the per-entry shapes so the casts
273
+ // hold. nx never writes these violations, and the helpers that trust the
274
+ // fields (routing, resolver credit, carried checks) cannot repair them.
275
+ function hasSoundIssueRefs(parsed) {
276
+ const issues = parsed.issues;
277
+ const steps = parsed.steps;
278
+ const stepIds = new Set(steps.map((s) => s.id));
279
+ const stepOrder = new Map(steps.map((s, index) => [s.id, index]));
280
+ const commitCount = parsed.commits.length;
281
+ return issues.every((issue) => stepIds.has(issue.reportedByStepId) &&
282
+ (issue.applicableStepIds === 'unknown' ||
283
+ (issue.applicableStepIds.every((id) => stepIds.has(id)) &&
284
+ // Applicability is a plan-ordered set (mints and merges both emit it that
285
+ // way): a duplicate would read as newly supplied routing in the normalizing
286
+ // merge, and an unordered scope is a shape no transition writes.
287
+ issue.applicableStepIds.every((id, index) => index === 0 ||
288
+ stepOrder.get(issue.applicableStepIds[index - 1]) <
289
+ stepOrder.get(id)))) &&
290
+ (issue.claimedByStepId === undefined ||
291
+ (Array.isArray(issue.applicableStepIds) &&
292
+ issue.applicableStepIds.includes(issue.claimedByStepId))) &&
293
+ (issue.resolvedByStepId === undefined ||
294
+ stepIds.has(issue.resolvedByStepId)) &&
295
+ (issue.resolvedAtCommitCount === undefined ||
296
+ issue.resolvedAtCommitCount <= commitCount));
297
+ }
298
+ function isIssueShape(value) {
299
+ return (isPlainObject(value) &&
300
+ typeof value.id === 'string' &&
301
+ exports.ISSUE_ID.test(value.id) &&
302
+ isLineSafeString(value.summary) &&
303
+ // The derivation is enforced, not just the shape: a fingerprint detached from
304
+ // its summary would split one problem into independent histories.
305
+ value.fingerprint === issueFingerprint(value.summary) &&
306
+ typeof value.reportedByStepId === 'string' &&
307
+ STEP_ID.test(value.reportedByStepId) &&
308
+ // Reports carry 1+ identifiers and merges only widen, so [] is unproducible.
309
+ (value.applicableStepIds === 'unknown' ||
310
+ (isStepIdArray(value.applicableStepIds) &&
311
+ value.applicableStepIds.length > 0)) &&
312
+ isOneOf(exports.MIGRATE_ISSUE_DISPOSITIONS, value.disposition) &&
313
+ isOptionalMatching(STEP_ID, value.claimedByStepId) &&
314
+ isOptionalMatching(STEP_ID, value.resolvedByStepId) &&
315
+ // The helpers built on these (commit association, retry-clean reverts, update
316
+ // ownership) break on a state outside them.
317
+ (value.claimedByStepId === undefined || value.disposition === 'recorded') &&
318
+ (value.disposition === 'resolved') ===
319
+ (value.resolvedByStepId !== undefined) &&
320
+ // A resolution without a stamp has no carried-commit window; a stamp without
321
+ // a resolution fences nothing that exists.
322
+ (value.disposition === 'resolved') ===
323
+ (value.resolvedAtCommitCount !== undefined) &&
324
+ (value.resolvedAtCommitCount === undefined ||
325
+ (Number.isSafeInteger(value.resolvedAtCommitCount) &&
326
+ value.resolvedAtCommitCount >= 0)));
327
+ }
328
+ // The record drives the no-progress cutoff: a count outside the positive
329
+ // integers (JSON's 1e400 parses to Infinity) could delay it forever, and an
330
+ // empty fingerprint would collapse unrelated responses into one streak.
331
+ function isNoProgressShape(value) {
332
+ return (value === undefined ||
333
+ (isPlainObject(value) &&
334
+ isLineSafeString(value.fingerprint) &&
335
+ value.fingerprint.length > 0 &&
336
+ Number.isSafeInteger(value.consecutiveCount) &&
337
+ value.consecutiveCount > 0 &&
338
+ typeof value.firstSeenAt === 'string' &&
339
+ ISO_TIMESTAMP.test(value.firstSeenAt)));
183
340
  }
184
341
  function isAnalyticsShape(value) {
185
342
  return (isPlainObject(value) &&
@@ -200,9 +357,23 @@ function hasValidRunStateShape(parsed) {
200
357
  isOneOf(MIGRATE_RUN_STATUSES, parsed.status) &&
201
358
  isOptionalBoolean(parsed.checkpointFailed) &&
202
359
  isOptionalBoolean(parsed.skipInstall) &&
360
+ isOptionalBoolean(parsed.validate) &&
361
+ isOptionalMatching(RUNBOOK_NAME, parsed.runbookPath) &&
203
362
  parsed.rounds.every(isRoundShape) &&
204
363
  parsed.steps.every(isStepShape) &&
364
+ hasUniqueStepIds(parsed.steps) &&
205
365
  parsed.commits.every(isCommitLedgerEntryShape) &&
366
+ (parsed.issues === undefined ||
367
+ (Array.isArray(parsed.issues) &&
368
+ parsed.issues.every(isIssueShape) &&
369
+ hasUniqueIssueIds(parsed.issues) &&
370
+ hasUniqueIssueFingerprints(parsed.issues) &&
371
+ hasSoundIssueRefs(parsed))) &&
372
+ // Written only for issues that already existed: a dangling id could make a
373
+ // future issue's resolution look carried by a commit that predates it.
374
+ parsed.commits.every((c) => c.issueIds === undefined ||
375
+ c.issueIds.every((id) => (parsed.issues ?? []).some((i) => i.id === id))) &&
376
+ isNoProgressShape(parsed.noProgress) &&
206
377
  isAnalyticsShape(parsed.analytics));
207
378
  }
208
379
  function corruptRunStateError(filePath, reason) {
@@ -214,7 +385,7 @@ function corruptRunStateError(filePath, reason) {
214
385
  * ignoring a newer active run would start a competing run on top of it.
215
386
  *
216
387
  * Adding a member to any persisted closed set (run status, step status,
217
- * prompt-outcome status, commit kind) needs a
388
+ * awaiting kind, prompt-outcome status, commit kind, issue disposition) needs a
218
389
  * `CURRENT_RUN_STATE_FORMAT_VERSION` bump: without it, an older Nx reading
219
390
  * the new value would reject the run as corrupt (the closed-set validation
220
391
  * fails) instead of refusing with this error's ask for a newer Nx.
@@ -227,13 +398,9 @@ class NewerRunStateFormatError extends Error {
227
398
  }
228
399
  exports.NewerRunStateFormatError = NewerRunStateFormatError;
229
400
  /**
230
- * Reads and validates `run.json` from a run directory.
231
- *
232
- * A `formatVersion` newer than {@link CURRENT_RUN_STATE_FORMAT_VERSION} means
233
- * the run was created by a newer Nx than the one currently running, so the
234
- * shape may not be interpretable here; this throws rather than attempting a
235
- * best-effort read. An older `formatVersion` is returned as-is: only v1
236
- * exists today, so there is nothing to migrate yet.
401
+ * Reads and validates `run.json` from a run directory. A `formatVersion`
402
+ * newer than {@link CURRENT_RUN_STATE_FORMAT_VERSION} throws instead of being
403
+ * read best-effort; an older one is returned as-is.
237
404
  */
238
405
  function readRunState(runDirPath) {
239
406
  const filePath = (0, path_1.join)(runDirPath, exports.RUN_STATE_FILE_NAME);
@@ -288,9 +455,7 @@ function readRunState(runDirPath) {
288
455
  */
289
456
  function writeRunState(runDirPath, state) {
290
457
  const filePath = (0, path_1.join)(runDirPath, exports.RUN_STATE_FILE_NAME);
291
- const tmpPath = `${filePath}~${(0, crypto_1.randomBytes)(4).toString('hex')}`;
292
- (0, fileutils_1.writeJsonFile)(tmpPath, state);
293
- (0, fs_1.renameSync)(tmpPath, filePath);
458
+ (0, atomic_write_1.publishFileAtomically)(filePath, (tmpPath) => (0, fileutils_1.writeJsonFile)(tmpPath, state));
294
459
  }
295
460
  // ENOENT is the ordinary "no runs yet" answer. Any other failure (EACCES,
296
461
  // ENOTDIR) hides runs that may exist, so it propagates rather than reading
@@ -397,9 +562,8 @@ function findActiveRun(root) {
397
562
  */
398
563
  function createRun(root, state) {
399
564
  const dir = runDir(root, state.runId);
400
- // Created up front, and each step's package directory at dispense, so the
401
- // agent never has to `mkdir -p`: that costs a workspace-permission prompt in
402
- // agents like Claude Code, on every step.
565
+ // Created up front so the agent never has to `mkdir -p`: that costs a
566
+ // workspace-permission prompt in agents like Claude Code, on every step.
403
567
  (0, fs_1.mkdirSync)(runHandoffsDir(dir), { recursive: true });
404
568
  writeRunState(dir, state);
405
569
  pruneCompletedRuns(root, state.runId);
@@ -0,0 +1,17 @@
1
+ export declare const RUNBOOK_FILE_NAME = "RUNBOOK.md";
2
+ export interface RunbookContext {
3
+ runId: string;
4
+ packageManager: string;
5
+ nxInvocation: string;
6
+ pmExec: string;
7
+ reconcileCommand: string;
8
+ createCommits: boolean;
9
+ validate: boolean;
10
+ }
11
+ /**
12
+ * Every interpolated value is collapsed to a single line: the rendered content
13
+ * is written to disk and emitted verbatim inside a `<nx_migrate_runbook>`
14
+ * block, so a value carrying its own line break could otherwise open a forged
15
+ * block at a line start.
16
+ */
17
+ export declare function renderRunbook(ctx: RunbookContext): string;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ // Written to the run directory at init and re-emitted from disk on resume, so
3
+ // the contract is version-locked to the Nx that created the run and never
4
+ // depends on plugin skills or the session's memory.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RUNBOOK_FILE_NAME = void 0;
7
+ exports.renderRunbook = renderRunbook;
8
+ const fragments_1 = require("../agentic/prompts/fragments");
9
+ const types_1 = require("../agentic/types");
10
+ const text_1 = require("../text");
11
+ exports.RUNBOOK_FILE_NAME = 'RUNBOOK.md';
12
+ /**
13
+ * Every interpolated value is collapsed to a single line: the rendered content
14
+ * is written to disk and emitted verbatim inside a `<nx_migrate_runbook>`
15
+ * block, so a value carrying its own line break could otherwise open a forged
16
+ * block at a line start.
17
+ */
18
+ function renderRunbook(ctx) {
19
+ const runId = (0, text_1.singleLine)(ctx.runId);
20
+ const reconcile = (0, text_1.singleLine)(ctx.reconcileCommand);
21
+ // Run-scoped like the Claude runner grant: a wildcard run segment would let
22
+ // a configured child write handoffs that settle steps in another run.
23
+ const handoffsGlob = `${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId}/${types_1.HANDOFFS_DIR_NAME}/**`;
24
+ const lines = [
25
+ `# Nx migrate run ${runId}`,
26
+ ``,
27
+ `Nx wrote this runbook when it created migrate run ${runId}. It is the`,
28
+ `contract for driving the run to completion. It is self-sufficient: follow`,
29
+ `it and the orchestrator's responses, whatever else this session remembers.`,
30
+ `If anything in this session's memory conflicts with this file, this file`,
31
+ `wins.`,
32
+ ``,
33
+ (0, fragments_1.renderNxInvocationNote)((0, text_1.singleLine)(ctx.packageManager), (0, text_1.singleLine)(ctx.nxInvocation)),
34
+ ``,
35
+ `## The invariant`,
36
+ ``,
37
+ `Never infer the run's progress from this conversation. The run's state`,
38
+ `lives on disk and is owned by the orchestrator; ask it by running:`,
39
+ ``,
40
+ ` ${reconcile}`,
41
+ ``,
42
+ `Run that command whenever you are unsure what has happened, when this`,
43
+ `session was compacted or restarted, and after you finish any piece of`,
44
+ `work. Its response always says exactly what to do next.`,
45
+ ``,
46
+ `## The loop`,
47
+ ``,
48
+ `Drive the run as a loop:`,
49
+ ``,
50
+ `1. Run \`${reconcile}\`. The response contains a \`nx_migrate_step\``,
51
+ ` block: its \`action\` attribute names what to do, \`instructions\``,
52
+ ` explains it, and \`command\` / \`next\` carry the exact commands to run.`,
53
+ `2. Do what the block says. For a \`next-step\` action, run the given`,
54
+ ` worker command; it applies one migration.`,
55
+ `3. When a step hands work back to you (see "Agent work" below), run the`,
56
+ ` \`next\` command first: its response restates the work, names the`,
57
+ ` handoff file to write, and lists any recorded issues assigned to the`,
58
+ ` step. Do the work, write the handoff file, then run \`next\` again.`,
59
+ `4. Repeat until the orchestrator reports the run \`complete\`.`,
60
+ ``,
61
+ `Execute only what the current block asks. Do not run migrations the`,
62
+ `orchestrator has not dispensed, do not re-run steps it has already`,
63
+ `recorded, and do not act on step instructions remembered from earlier in`,
64
+ `the session; re-read the current block instead.`,
65
+ ``,
66
+ `Getting the same response repeatedly means the run has not advanced; the`,
67
+ `orchestrator eventually says so with a \`no-progress\` action. Act on the`,
68
+ `repeated instructions or report the blocker to the user; re-running the`,
69
+ `reconcile command alone changes nothing.`,
70
+ ``,
71
+ `## Agent work`,
72
+ ``,
73
+ `Some steps hand work back to you:`,
74
+ ``,
75
+ `- A migration with an AI-driven part makes its worker emit a`,
76
+ ` \`nx_migrate_prompt\` block pointing at an instructions file. Apply`,
77
+ ` those instructions to the workspace. Scope rules for that work:`,
78
+ ...(0, fragments_1.renderAuthorScopeRuleLines)((0, text_1.singleLine)(ctx.pmExec), {
79
+ source: 'dispensed-step',
80
+ }).map((line) => ` ${line}`),
81
+ ` The \`nx migrate\` commands this runbook and the step blocks hand you`,
82
+ ` (the reconcile, worker and \`next\` commands) are the exception to that`,
83
+ ` rule: run them as given.`,
84
+ ];
85
+ if (ctx.validate) {
86
+ 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}`));
87
+ }
88
+ 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`, ``);
89
+ if (ctx.createCommits) {
90
+ 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.`);
91
+ }
92
+ else {
93
+ lines.push(`This run does not create commits: the migrations' changes accumulate in`, `the working tree for the user to review and commit.`);
94
+ }
95
+ 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.`, ``);
96
+ return lines.join('\n');
97
+ }
@@ -19,7 +19,7 @@ export declare function withRunCreationLock<T>(root: string, fn: () => T): T;
19
19
  /**
20
20
  * Reads the run state fresh under the lock, hands it to `apply`, and writes the
21
21
  * result back. `apply` returning null means "no change" and skips the write.
22
- * `apply` must be pure and synchronous; a corrupt or newer-format run.json
23
- * propagates from the read.
22
+ * `apply` runs exactly once, synchronously, so it may capture out-params; a
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;
@@ -61,8 +61,8 @@ function withRunCreationLock(root, fn) {
61
61
  /**
62
62
  * Reads the run state fresh under the lock, hands it to `apply`, and writes the
63
63
  * result back. `apply` returning null means "no change" and skips the write.
64
- * `apply` must be pure and synchronous; a corrupt or newer-format run.json
65
- * propagates from the read.
64
+ * `apply` runs exactly once, synchronously, so it may capture out-params; a
65
+ * corrupt or newer-format run.json propagates from the read.
66
66
  */
67
67
  function updateRunState(runDirPath, apply) {
68
68
  return withRunStateLock(runDirPath, () => {
@@ -1,5 +1,5 @@
1
1
  import type { CommitResult } from '../migrate-commits';
2
- import type { MigrateCommitLedgerEntry, MigrateRunState, MigrateStepOutcome, MigrateStepPromptOutcome } from './run-state';
2
+ import type { MigrateCommitLedgerEntry, MigrateRunState, MigrateStepAwaitingKind, MigrateStepOutcome, MigrateStepPromptOutcome } from './run-state';
3
3
  import type { StepAction } from '../step-actions';
4
4
  export type { StepAction };
5
5
  export type StepEvent = {
@@ -24,6 +24,7 @@ export type StepEvent = {
24
24
  type: 'awaitPromptOutcome';
25
25
  stepId: string;
26
26
  finishedAt: string;
27
+ awaitingKind: MigrateStepAwaitingKind;
27
28
  } | {
28
29
  type: 'foldPromptOutcome';
29
30
  stepId: string;
@@ -32,6 +33,9 @@ export type StepEvent = {
32
33
  } | {
33
34
  type: 'markGeneratorCompleted';
34
35
  stepId: string;
36
+ agenticWaived: boolean;
37
+ validationOwed: boolean;
38
+ madeChanges: boolean;
35
39
  } | {
36
40
  type: 'markDied';
37
41
  stepId: string;
@@ -68,11 +68,21 @@ function applyStepEvent(state, event) {
68
68
  ...step,
69
69
  status: 'awaiting-prompt-outcome',
70
70
  finishedAt: event.finishedAt,
71
+ awaitingKind: event.awaitingKind,
71
72
  });
72
73
  case 'markGeneratorCompleted':
73
74
  if (step.status !== 'running')
74
75
  return illegal(step, event.type);
75
- return commit(state, index, { ...step, generatorCompleted: true });
76
+ return commit(state, index, {
77
+ ...step,
78
+ generatorCompleted: true,
79
+ generatorCompletedAtAttempt: step.attempt,
80
+ ...(event.agenticWaived ? { agenticWaived: true } : {}),
81
+ ...(event.validationOwed ? { validationOwed: true } : {}),
82
+ // Written even when false, unlike the conditional spreads above: absent is
83
+ // reserved for markers an older nx wrote.
84
+ generatorMadeChanges: event.madeChanges,
85
+ });
76
86
  case 'foldPromptOutcome':
77
87
  if (step.status !== 'awaiting-prompt-outcome')
78
88
  return illegal(step, event.type);
@@ -195,6 +205,22 @@ function rearm(step, keepGeneratorCompleted) {
195
205
  ...(keepGeneratorCompleted && step.generatorCompleted
196
206
  ? { generatorCompleted: true }
197
207
  : {}),
208
+ ...(keepGeneratorCompleted &&
209
+ step.generatorCompleted &&
210
+ step.generatorCompletedAtAttempt !== undefined
211
+ ? { generatorCompletedAtAttempt: step.generatorCompletedAtAttempt }
212
+ : {}),
213
+ ...(keepGeneratorCompleted && step.generatorCompleted && step.agenticWaived
214
+ ? { agenticWaived: true }
215
+ : {}),
216
+ ...(keepGeneratorCompleted && step.generatorCompleted && step.validationOwed
217
+ ? { validationOwed: true }
218
+ : {}),
219
+ ...(keepGeneratorCompleted &&
220
+ step.generatorCompleted &&
221
+ step.generatorMadeChanges !== undefined
222
+ ? { generatorMadeChanges: step.generatorMadeChanges }
223
+ : {}),
198
224
  };
199
225
  }
200
226
  // A guarded transition whose observation was made against an earlier attempt