nx 23.3.0-beta.0 → 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 (151) 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/report/report.d.ts +2 -0
  55. package/dist/src/command-line/report/report.js +28 -1
  56. package/dist/src/command-line/yargs-utils/shared-options.d.ts +1 -1
  57. package/dist/src/command-line/yargs-utils/shared-options.js +22 -4
  58. package/dist/src/config/workspace-json-project-json.d.ts +2 -0
  59. package/dist/src/core/graph/main.js +1 -1
  60. package/dist/src/daemon/client/client.d.ts +4 -2
  61. package/dist/src/daemon/client/client.js +20 -11
  62. package/dist/src/daemon/message-types/run-tasks-execution-hooks.d.ts +3 -2
  63. package/dist/src/daemon/server/handle-hash-tasks.d.ts +8 -2
  64. package/dist/src/daemon/server/handle-hash-tasks.js +16 -3
  65. package/dist/src/daemon/server/handle-outputs-changes.d.ts +2 -2
  66. package/dist/src/daemon/server/handle-outputs-changes.js +21 -6
  67. package/dist/src/daemon/server/handle-tasks-execution-hooks.d.ts +3 -2
  68. package/dist/src/daemon/server/outputs-tracking.d.ts +6 -0
  69. package/dist/src/daemon/server/outputs-tracking.js +18 -0
  70. package/dist/src/daemon/server/project-graph-incremental-recomputation.d.ts +22 -7
  71. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +99 -69
  72. package/dist/src/daemon/server/restart-checks.d.ts +22 -0
  73. package/dist/src/daemon/server/restart-checks.js +77 -0
  74. package/dist/src/daemon/server/server.js +28 -18
  75. package/dist/src/daemon/server/shutdown-utils.d.ts +0 -5
  76. package/dist/src/daemon/server/shutdown-utils.js +3 -26
  77. package/dist/src/hasher/hash-task.js +20 -16
  78. package/dist/src/hasher/native-task-hasher-impl.d.ts +8 -0
  79. package/dist/src/hasher/native-task-hasher-impl.js +46 -3
  80. package/dist/src/hasher/task-hasher.d.ts +9 -0
  81. package/dist/src/hasher/task-hasher.js +14 -0
  82. package/dist/src/native/index.d.ts +202 -21
  83. package/dist/src/native/index.js +3 -3
  84. package/dist/src/native/native-bindings.js +3 -1
  85. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  86. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  87. package/dist/src/plugins/js/lock-file/pnpm-parser.js +1 -1
  88. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.d.ts +2 -0
  89. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.js +22 -0
  90. package/dist/src/plugins/js/utils/register.js +2 -1
  91. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +2 -1
  92. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +3 -2
  93. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +5 -1
  94. package/dist/src/project-graph/plugins/loaded-nx-plugin.d.ts +3 -2
  95. package/dist/src/project-graph/plugins/loaded-nx-plugin.js +5 -1
  96. package/dist/src/project-graph/plugins/task-results-stub.d.ts +50 -0
  97. package/dist/src/project-graph/plugins/task-results-stub.js +111 -0
  98. package/dist/src/project-graph/plugins/tasks-execution-hooks.d.ts +3 -2
  99. package/dist/src/project-graph/plugins/tasks-execution-hooks.js +2 -1
  100. package/dist/src/project-graph/project-graph.js +2 -0
  101. package/dist/src/tasks-runner/batch/run-batch.js +15 -0
  102. package/dist/src/tasks-runner/cache.d.ts +35 -0
  103. package/dist/src/tasks-runner/cache.js +40 -0
  104. package/dist/src/tasks-runner/create-task-graph.js +1 -0
  105. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  106. package/dist/src/tasks-runner/forked-process-task-runner.d.ts +1 -1
  107. package/dist/src/tasks-runner/forked-process-task-runner.js +8 -7
  108. package/dist/src/tasks-runner/init-tasks-runner.js +5 -1
  109. package/dist/src/tasks-runner/is-tui-enabled.js +4 -3
  110. package/dist/src/tasks-runner/life-cycle.d.ts +7 -0
  111. package/dist/src/tasks-runner/life-cycle.js +7 -0
  112. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.d.ts +35 -0
  113. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.js +146 -0
  114. package/dist/src/tasks-runner/prune-task-graph.d.ts +17 -0
  115. package/dist/src/tasks-runner/prune-task-graph.js +45 -0
  116. package/dist/src/tasks-runner/run-command.js +14 -3
  117. package/dist/src/tasks-runner/running-tasks/batch-process.d.ts +93 -19
  118. package/dist/src/tasks-runner/running-tasks/batch-process.js +184 -88
  119. package/dist/src/tasks-runner/task-orchestrator.d.ts +64 -2
  120. package/dist/src/tasks-runner/task-orchestrator.js +232 -46
  121. package/dist/src/tasks-runner/terminal-output-path.d.ts +11 -0
  122. package/dist/src/tasks-runner/terminal-output-path.js +18 -0
  123. package/dist/src/tasks-runner/utils.js +5 -2
  124. package/dist/src/utils/ab-testing.d.ts +3 -0
  125. package/dist/src/utils/ab-testing.js +11 -3
  126. package/dist/src/utils/chunkify.d.ts +1 -7
  127. package/dist/src/utils/chunkify.js +2 -8
  128. package/dist/src/utils/command-line-utils.d.ts +19 -0
  129. package/dist/src/utils/command-line-utils.js +10 -3
  130. package/dist/src/utils/formatters/index.d.ts +6 -0
  131. package/dist/src/utils/formatters/index.js +5 -0
  132. package/dist/src/utils/formatters/prettier.d.ts +0 -8
  133. package/dist/src/utils/formatters/prettier.js +4 -26
  134. package/dist/src/utils/installed-nx-version.js +2 -48
  135. package/dist/src/utils/min-release-age/packument.js +1 -1
  136. package/dist/src/utils/output.d.ts +22 -3
  137. package/dist/src/utils/output.js +28 -5
  138. package/dist/src/utils/package-json.d.ts +25 -0
  139. package/dist/src/utils/package-json.js +41 -6
  140. package/dist/src/utils/package-manager.d.ts +6 -0
  141. package/dist/src/utils/package-manager.js +10 -0
  142. package/dist/src/utils/provenance.js +20 -4
  143. package/dist/src/utils/workspace-context.d.ts +47 -3
  144. package/dist/src/utils/workspace-context.js +162 -10
  145. package/package.json +12 -12
  146. package/schemas/nx-schema.json +4 -0
  147. package/schemas/project-schema.json +4 -0
  148. package/dist/src/daemon/server/file-watching/route-workspace-changes.d.ts +0 -9
  149. package/dist/src/daemon/server/file-watching/route-workspace-changes.js +0 -76
  150. package/dist/src/daemon/server/watcher.d.ts +0 -18
  151. package/dist/src/daemon/server/watcher.js +0 -126
@@ -18,15 +18,6 @@ const ignore_1 = require("../../utils/ignore");
18
18
  const object_sort_1 = require("../../utils/object-sort");
19
19
  const output_1 = require("../../utils/output");
20
20
  const workspace_root_1 = require("../../utils/workspace-root");
21
- /**
22
- * A table, not a `switch`: this lookup sits inside a `try` whose `catch`
23
- * reports "configured but not installed", and a `never` arm would throw into
24
- * that catch and be misreported. A missing member is a compile error here.
25
- */
26
- const resolveFormatterBin = {
27
- oxfmt: oxfmt_1.getOxfmtBinPath,
28
- prettier: prettier_1.getPrettierPath,
29
- };
30
21
  async function format(command, args) {
31
22
  const formatterType = (0, formatters_1.detectFormatter)(workspace_root_1.workspaceRoot);
32
23
  if (!formatterType) {
@@ -41,7 +32,7 @@ async function format(command, args) {
41
32
  // node_modules. Resolving now turns a raw MODULE_NOT_FOUND into something
42
33
  // actionable.
43
34
  try {
44
- resolveFormatterBin[formatterType]();
35
+ formatters_1.resolveFormatterBin[formatterType]();
45
36
  }
46
37
  catch {
47
38
  output_1.output.error({
@@ -53,19 +44,12 @@ async function format(command, args) {
53
44
  process.exit(1);
54
45
  }
55
46
  const { nxArgs } = (0, command_line_utils_1.splitArgsIntoNxArgsAndOverrides)(args, 'affected', { printWarnings: false }, (0, configuration_1.readNxJson)());
56
- // Patterns are kept raw here. Prettier is invoked through a shell so it
57
- // quotes them at the call site; oxfmt is invoked with execFile and needs
58
- // the unquoted paths.
59
47
  const patterns = await getPatterns(formatterType, {
60
48
  ...args,
61
49
  ...nxArgs,
62
50
  });
63
51
  // Chunkify the patterns array to prevent crashing the windows terminal.
64
- // The prettier path quotes each pattern on its way to the shell, so size the
65
- // chunks against that; oxfmt goes through execFile and gets them raw.
66
- const chunkList = (0, chunkify_1.chunkify)(patterns, undefined, formatterType === 'prettier'
67
- ? (pattern) => (0, prettier_1.quoteForShell)(pattern).length
68
- : undefined);
52
+ const chunkList = (0, chunkify_1.chunkify)(patterns);
69
53
  switch (command) {
70
54
  case 'write':
71
55
  if (nxArgs.sortRootTsconfigPaths) {
@@ -850,17 +850,51 @@ async function getExpandedTaskInputs(depGraphClientResponse, expandedTaskInputsC
850
850
  expandedTaskInputsCache.set(taskId, result);
851
851
  return result;
852
852
  }
853
+ // A brace group ({a,b}.json) carries commas, so a glob list joined with
854
+ // commas can only be split at brace depth zero.
855
+ function splitGlobGroup(group) {
856
+ const globs = [];
857
+ let current = '';
858
+ let depth = 0;
859
+ for (const char of group) {
860
+ if (char === '{') {
861
+ depth++;
862
+ }
863
+ else if (char === '}' && depth > 0) {
864
+ depth--;
865
+ }
866
+ if (char === ',' && depth === 0) {
867
+ globs.push(current);
868
+ current = '';
869
+ }
870
+ else {
871
+ current += char;
872
+ }
873
+ }
874
+ globs.push(current);
875
+ // An unbalanced `{` would swallow the rest of the list; a plain split is
876
+ // the better guess then.
877
+ return depth === 0 ? globs : group.split(',');
878
+ }
853
879
  function expandInputs(inputs, project, allWorkspaceFiles, depGraphClientResponse) {
854
880
  const projectNames = depGraphClientResponse.projects.map((p) => p.name);
855
881
  const workspaceRootInputs = [];
856
882
  const projectRootInputs = [];
857
883
  const externalInputs = [];
858
884
  const otherInputs = [];
885
+ const filesInputs = [];
859
886
  inputs.forEach((input) => {
860
887
  // grouped workspace inputs look like workspace:[pattern,otherPattern]
861
888
  if (input.startsWith('workspace:[')) {
862
- const inputs = input.substring(11, input.length - 1).split(',');
863
- workspaceRootInputs.push(...inputs);
889
+ workspaceRootInputs.push(...splitGlobGroup(input.substring(11, input.length - 1)));
890
+ return;
891
+ }
892
+ // Disk-backed groups look like files:[glob,!otherGlob]. They expand on
893
+ // disk, so they must be matched before the `:` catch-all below classifies
894
+ // them as external dependencies.
895
+ const diskBacked = /^files:\[(.*)\]$/.exec(input);
896
+ if (diskBacked) {
897
+ filesInputs.push(splitGlobGroup(diskBacked[1]));
864
898
  return;
865
899
  }
866
900
  const maybeProjectName = input.split(':')[0];
@@ -881,6 +915,7 @@ function expandInputs(inputs, project, allWorkspaceFiles, depGraphClientResponse
881
915
  }
882
916
  });
883
917
  const workspaceRootsExpanded = getExpandedWorkspaceRoots(workspaceRootInputs, allWorkspaceFiles);
918
+ const filesExpanded = filesInputs.flatMap((globs) => (0, native_1.expandFilesInput)(workspace_root_1.workspaceRoot, globs));
884
919
  const otherInputsExpanded = otherInputs.map((input) => {
885
920
  if (input === 'TsConfig') {
886
921
  return (0, node_path_1.relative)(workspace_root_1.workspaceRoot, (0, typescript_1.getRootTsConfigPath)());
@@ -908,7 +943,11 @@ function expandInputs(inputs, project, allWorkspaceFiles, depGraphClientResponse
908
943
  return acc;
909
944
  }, {});
910
945
  return {
911
- general: [...workspaceRootsExpanded, ...otherInputsExpanded],
946
+ general: [
947
+ ...workspaceRootsExpanded,
948
+ ...filesExpanded,
949
+ ...otherInputsExpanded,
950
+ ],
912
951
  ...projectRootsExpanded,
913
952
  external: externalInputs,
914
953
  };
@@ -72,8 +72,7 @@ command = 'nx init') {
72
72
  // already finished.
73
73
  //
74
74
  // Chunked as `nx format` chunks: the Angular flow records a `project.json`
75
- // per project. The prettier path is sized against its quoted length; oxfmt
76
- // goes through execFile and gets raw paths.
75
+ // per project.
77
76
  let formatter = null;
78
77
  try {
79
78
  formatter = (0, formatters_1.detectFormatter)(repoRoot);
@@ -93,7 +92,7 @@ command = 'nx init') {
93
92
  // batch, but exits 2 on one unsupported file, which would end a
94
93
  // successful init with a spurious warning and prettier's own stderr.
95
94
  const supported = await (0, prettier_1.filterToPrettierSupportedFiles)(files);
96
- for (const chunk of (0, chunkify_1.chunkify)(supported, undefined, (pattern) => (0, prettier_1.quoteForShell)(pattern).length)) {
95
+ for (const chunk of (0, chunkify_1.chunkify)(supported)) {
97
96
  if (chunk.length) {
98
97
  (0, prettier_1.writeWithPrettier)(chunk, repoRoot);
99
98
  }
@@ -4,6 +4,7 @@ exports.AGENT_DEFINITIONS = exports.opencodeDefinition = exports.codexDefinition
4
4
  exports.getAgentDefinition = getAgentDefinition;
5
5
  const os_1 = require("os");
6
6
  const path_1 = require("path");
7
+ const smol_toml_1 = require("smol-toml");
7
8
  const types_1 = require("./types");
8
9
  // --- Claude Code ---------------------------------------------------------
9
10
  function claudeCodeWellKnownPaths() {
@@ -49,15 +50,14 @@ function claudeCodeHandoffAllowedTools(runDirName) {
49
50
  function claudeCodeBuildInteractive(ctx) {
50
51
  const allowedTools = claudeCodeHandoffAllowedTools(ctx.runDirName);
51
52
  return {
52
- // `--allowedTools` is variadic (space/comma separated): a positional
53
- // placed right after its value gets swallowed as another rule. The rules
54
- // must stay in one comma-joined element with a non-variadic flag
55
- // (`--system-prompt`) between them and the user prompt.
53
+ // `--allowedTools` is variadic, so a positional right after it is read as
54
+ // another rule: keep the rules in one comma-joined element and a flag
55
+ // between them and the instructions pointer.
56
56
  args: [
57
57
  ...(allowedTools ? ['--allowedTools', allowedTools] : []),
58
- '--system-prompt',
59
- ctx.systemContext,
60
- ctx.userPrompt,
58
+ '--system-prompt-file',
59
+ ctx.systemPromptFilePath,
60
+ ctx.instructionsPointer,
61
61
  ],
62
62
  cwd: ctx.workspaceRoot,
63
63
  };
@@ -74,14 +74,41 @@ function codexWellKnownPaths() {
74
74
  return [];
75
75
  }
76
76
  // No handoff permission flag: codex's default sandbox already allows writes
77
- // inside the cwd tree without prompting, and a user-hardened read-only config
78
- // is a deliberate choice we don't override.
77
+ // inside the cwd tree, and a user-hardened read-only config is theirs to keep.
78
+ //
79
+ // `-c model_instructions_file` replaces codex's built-in instructions rather
80
+ // than adding this context to them, so the context stays on the command line,
81
+ // reduced rather than the full prompt.
79
82
  function codexBuildInteractive(ctx) {
80
83
  return {
81
- args: ['-c', `developer_instructions=${ctx.systemContext}`, ctx.userPrompt],
84
+ args: [
85
+ '-c',
86
+ `developer_instructions=${encodeTomlString(ctx.inlineSystemContext)}`,
87
+ ctx.instructionsPointer,
88
+ ],
82
89
  cwd: ctx.workspaceRoot,
83
90
  };
84
91
  }
92
+ /**
93
+ * Encodes a single-line TOML string for codex overrides. Codex treats a parse
94
+ * failure as literal text instead of reporting it, so the result is parsed
95
+ * back before it reaches the command line.
96
+ */
97
+ function encodeTomlString(value) {
98
+ // JSON escapes every control TOML rejects except DEL, which it leaves raw.
99
+ const encoded = JSON.stringify(value).replace(/\x7f/g, '\\u007F');
100
+ let decoded;
101
+ try {
102
+ decoded = (0, smol_toml_1.parse)(`value = ${encoded}`).value;
103
+ }
104
+ catch (err) {
105
+ throw new Error(`Could not encode the agent's system context as TOML for OpenAI Codex: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
106
+ }
107
+ if (decoded !== value) {
108
+ throw new Error(`Encoding the agent's system context as TOML for OpenAI Codex did not round-trip; refusing to run the agent on altered instructions.`);
109
+ }
110
+ return encoded;
111
+ }
85
112
  exports.codexDefinition = {
86
113
  id: 'codex',
87
114
  displayName: 'OpenAI Codex',
@@ -110,25 +137,54 @@ function opencodeWellKnownPaths() {
110
137
  return candidates;
111
138
  }
112
139
  // No handoff permission config: opencode's `edit` permission defaults to
113
- // allow, and injecting one would clobber (not merge with) a user's own
114
- // permission patterns.
140
+ // allow, and injecting one would clobber a user's own patterns rather than
141
+ // merge with them.
142
+ //
143
+ // The config travels through OPENCODE_CONFIG_CONTENT rather than
144
+ // OPENCODE_CONFIG. Both merge with the user's own config, but the runner
145
+ // spreads `env` over `process.env`, so the latter would overwrite a value the
146
+ // user had set.
115
147
  function opencodeBuildInteractive(ctx) {
116
- const config = {
117
- agent: {
118
- [OPENCODE_TRANSIENT_AGENT_NAME]: { prompt: ctx.systemContext },
119
- },
120
- };
121
148
  return {
122
149
  args: [
123
150
  '--agent',
124
151
  OPENCODE_TRANSIENT_AGENT_NAME,
125
152
  '--prompt',
126
- ctx.userPrompt,
153
+ ctx.instructionsPointer,
127
154
  ],
128
- env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(config) },
155
+ env: { OPENCODE_CONFIG_CONTENT: opencodeConfigContent(ctx) },
129
156
  cwd: ctx.workspaceRoot,
130
157
  };
131
158
  }
159
+ /**
160
+ * Escapes inline prompt patterns before opencode's pre-JSON substitution pass.
161
+ * Excludes generator instructions from the config; cmd.exe drops inherited
162
+ * variables longer than 8191 characters.
163
+ */
164
+ function opencodeConfigContent(ctx) {
165
+ // Windows separators become `/`, which its APIs accept just as well.
166
+ // Elsewhere a `\` belongs to the file name.
167
+ const filePath = process.platform === 'win32'
168
+ ? ctx.systemPromptFilePath.replace(/\\/g, '/')
169
+ : ctx.systemPromptFilePath;
170
+ const prompt = isSubstitutionSafePath(filePath)
171
+ ? JSON.stringify(`{file:${filePath}}`)
172
+ : // `\u007b` starts no pattern and JSON decodes it back to `{`, so the
173
+ // prompt survives expansion whatever the workspace path put in it.
174
+ JSON.stringify(ctx.systemPrompt).replace(/\{/g, '\\u007b');
175
+ // Assembled rather than serialized whole: `JSON.stringify` over the object
176
+ // would re-escape the backslashes the `\u007b` encoding introduced.
177
+ return `{"agent":{${JSON.stringify(OPENCODE_TRANSIENT_AGENT_NAME)}:{"prompt":${prompt}}}}`;
178
+ }
179
+ /**
180
+ * Whether opencode would read back the path nx wrote. A `}` closes the
181
+ * reference early, and a character JSON escapes arrives escaped. Braces are
182
+ * rejected wholesale rather than matched against `{env:` and `{file:`, since a
183
+ * pattern opening inside the path swallows the reference's closing brace.
184
+ */
185
+ function isSubstitutionSafePath(filePath) {
186
+ return (!/[{}]/.test(filePath) && JSON.stringify(filePath).slice(1, -1) === filePath);
187
+ }
132
188
  exports.opencodeDefinition = {
133
189
  id: 'opencode',
134
190
  displayName: 'OpenCode',
@@ -0,0 +1,14 @@
1
+ import { FormatterType } from '../../../utils/formatters';
2
+ /**
3
+ * The command the agent runs over the files it changed, with a `<paths>`
4
+ * placeholder. `--` keeps a path that starts with `-` from being read as an
5
+ * option.
6
+ */
7
+ export declare function formatCommandFor(formatter: FormatterType, pmExec: string): string;
8
+ /**
9
+ * Resolved per dispense so a migration's formatter change takes effect; the
10
+ * scope rules name the replacement commands for the step that made it. `null`
11
+ * when no formatter is configured or its binary cannot be resolved: under npm
12
+ * the command would otherwise make `npx` download it.
13
+ */
14
+ export declare function resolveFormatCommand(root: string, pmExec: string): string | null;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCommandFor = formatCommandFor;
4
+ exports.resolveFormatCommand = resolveFormatCommand;
5
+ const formatters_1 = require("../../../utils/formatters");
6
+ /**
7
+ * The command the agent runs over the files it changed, with a `<paths>`
8
+ * placeholder. `--` keeps a path that starts with `-` from being read as an
9
+ * option.
10
+ */
11
+ function formatCommandFor(formatter, pmExec) {
12
+ switch (formatter) {
13
+ case 'prettier':
14
+ // --ignore-unknown skips files prettier has no parser for.
15
+ return `${pmExec} prettier --write --ignore-unknown -- <paths>`;
16
+ case 'oxfmt':
17
+ // oxfmt exits 2 when every path was skipped; the flag makes that
18
+ // success. Unlike prettier it skips unparseable files on its own.
19
+ return `${pmExec} oxfmt --no-error-on-unmatched-pattern -- <paths>`;
20
+ default: {
21
+ const unhandled = formatter;
22
+ throw new Error(`Unhandled formatter: ${unhandled}`);
23
+ }
24
+ }
25
+ }
26
+ /**
27
+ * Resolved per dispense so a migration's formatter change takes effect; the
28
+ * scope rules name the replacement commands for the step that made it. `null`
29
+ * when no formatter is configured or its binary cannot be resolved: under npm
30
+ * the command would otherwise make `npx` download it.
31
+ */
32
+ function resolveFormatCommand(root, pmExec) {
33
+ const formatter = (0, formatters_1.detectFormatter)(root);
34
+ if (formatter === null) {
35
+ return null;
36
+ }
37
+ try {
38
+ formatters_1.resolveFormatterBin[formatter]();
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ return formatCommandFor(formatter, pmExec);
44
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Both the agentic runner and the orchestrator write per-run scratch under
3
- * `.nx/migrate-runs/<run-id>/`: handoff files in both cases, plus the durable
4
- * run state and its plan snapshots for the orchestrator. The v23 migration
5
- * `23-0-0-add-migrate-runs-to-git-ignore` adds `.nx/migrate-runs` to
3
+ * `.nx/migrate-runs/<run-id>/`: handoff and prompt files in both cases, plus
4
+ * the durable run state and its plan snapshots for the orchestrator. The v23
5
+ * migration `23-0-0-add-migrate-runs-to-git-ignore` adds `.nx/migrate-runs` to
6
6
  * `.gitignore`; in its declared slot (typically late) earlier per-migration
7
7
  * commits would absorb the scratch into the user-visible diff.
8
8
  *
@@ -11,9 +11,9 @@ const logger_1 = require("../../../utils/logger");
11
11
  const types_1 = require("./types");
12
12
  /**
13
13
  * Both the agentic runner and the orchestrator write per-run scratch under
14
- * `.nx/migrate-runs/<run-id>/`: handoff files in both cases, plus the durable
15
- * run state and its plan snapshots for the orchestrator. The v23 migration
16
- * `23-0-0-add-migrate-runs-to-git-ignore` adds `.nx/migrate-runs` to
14
+ * `.nx/migrate-runs/<run-id>/`: handoff and prompt files in both cases, plus
15
+ * the durable run state and its plan snapshots for the orchestrator. The v23
16
+ * migration `23-0-0-add-migrate-runs-to-git-ignore` adds `.nx/migrate-runs` to
17
17
  * `.gitignore`; in its declared slot (typically late) earlier per-migration
18
18
  * commits would absorb the scratch into the user-visible diff.
19
19
  *
@@ -1,3 +1,4 @@
1
+ import { type BigIntStats } from 'fs';
1
2
  import { HandoffFile } from './types';
2
3
  /** Returns the run directory for a given workspace + run id (target version). */
3
4
  export declare function runDirPath(workspaceRoot: string, runId: string): string;
@@ -19,19 +20,16 @@ export declare function mkdirSafely(dir: string, purpose: string): void;
19
20
  * inspection.
20
21
  */
21
22
  export declare function initRunDir(workspaceRoot: string, runId: string): string;
22
- /**
23
- * Absolute path of the handoff file for a migration step within a run, under
24
- * the run directory's `handoffs/` subtree: the pre-authorized write scope
25
- * stops there, so a handoff placed anywhere else costs an approval prompt.
26
- * The package's scope (if any) becomes a real subdirectory so the package name
27
- * stays readable; two packages can ship a migration with the same name without
28
- * colliding because they land in different package subdirectories. Each
29
- * segment is sanitized so the path is always writable on every platform.
30
- */
31
23
  export declare function stepHandoffPath(runDir: string, migration: {
32
24
  package: string;
33
25
  name: string;
34
26
  }): string;
27
+ /** Handoff path for a run step. No hash needed: step ids are unique within the run. */
28
+ export declare function runStepHandoffPath(runDir: string, stepId: string): string;
29
+ export declare function stepPromptsDir(runDir: string, migration: {
30
+ package: string;
31
+ name: string;
32
+ }): string;
35
33
  export type HandoffReadFailureReason = 'missing' | 'read-error' | 'parse-error' | 'shape-mismatch';
36
34
  export type HandoffReadResult = {
37
35
  ok: true;
@@ -42,19 +40,31 @@ export type HandoffReadResult = {
42
40
  detail?: string;
43
41
  };
44
42
  /**
45
- * Reads and validates a handoff file written by an agent. Returns a tagged
46
- * result so callers (the in-loop poller and the post-exit resolver) can
47
- * distinguish "file not yet written" from "file written but garbage" — the
48
- * latter is surfaced to the user instead of being collapsed into the same
49
- * generic ambiguous-outcome prompt.
43
+ * `lstat`, not `stat`: a symlink in the handoffs dir's place would send every
44
+ * handoff read and removal wherever it points.
45
+ */
46
+ export declare function handoffsDirState(handoffsDir: string): 'directory' | 'missing' | 'other';
47
+ /**
48
+ * Reads the file `stat` describes, refusing a symlink swapped in after the
49
+ * caller's lstat: O_NOFOLLOW fails the open with ELOOP, and O_NONBLOCK keeps a
50
+ * planted FIFO from blocking it. Windows has neither flag, so there the inode
51
+ * comparison is what catches a followed symlink. It does not guard against an
52
+ * unlink and recreate reusing the inode number. Read errors propagate: a file
53
+ * the agent cannot read must not pass.
54
+ */
55
+ export declare function readInspectedFile(filePath: string, stat: BigIntStats, replacedMessage: string): string;
56
+ /**
57
+ * Splits "not written yet" from "written but garbage" so callers can surface a
58
+ * malformed handoff instead of collapsing it into the generic
59
+ * ambiguous-outcome prompt.
50
60
  */
51
- export declare function readHandoffWithReason(filePath: string): HandoffReadResult;
61
+ export declare function readHandoffWithReason(filePath: string, handoffsDir: string): HandoffReadResult;
52
62
  /**
53
63
  * Convenience wrapper preserving the original null-on-any-failure contract.
54
64
  * Used by the polling loop (`waitForValidHandoff`) where every failure mode
55
65
  * is "keep waiting" — the file may be missing, mid-write, or being rewritten.
56
66
  */
57
- export declare function readHandoff(filePath: string): HandoffFile | null;
67
+ export declare function readHandoff(filePath: string, handoffsDir: string): HandoffFile | null;
58
68
  /**
59
69
  * Polls for a valid handoff file. Resolves once `readHandoff` accepts the
60
70
  * file's contents. Used to detect when the agent has finished its work so the
@@ -63,7 +73,7 @@ export declare function readHandoff(filePath: string): HandoffFile | null;
63
73
  *
64
74
  * Rejects with the abort reason when `options.signal` is aborted.
65
75
  */
66
- export declare function waitForValidHandoff(handoffFilePath: string, options?: {
76
+ export declare function waitForValidHandoff(handoffFilePath: string, handoffsDir: string, options?: {
67
77
  intervalMs?: number;
68
78
  signal?: AbortSignal;
69
79
  }): Promise<void>;
@@ -5,9 +5,14 @@ exports.resolveAgenticRunId = resolveAgenticRunId;
5
5
  exports.mkdirSafely = mkdirSafely;
6
6
  exports.initRunDir = initRunDir;
7
7
  exports.stepHandoffPath = stepHandoffPath;
8
+ exports.runStepHandoffPath = runStepHandoffPath;
9
+ exports.stepPromptsDir = stepPromptsDir;
10
+ exports.handoffsDirState = handoffsDirState;
11
+ exports.readInspectedFile = readInspectedFile;
8
12
  exports.readHandoffWithReason = readHandoffWithReason;
9
13
  exports.readHandoff = readHandoff;
10
14
  exports.waitForValidHandoff = waitForValidHandoff;
15
+ const crypto_1 = require("crypto");
11
16
  const fs_1 = require("fs");
12
17
  const path_1 = require("path");
13
18
  const semver_1 = require("semver");
@@ -56,14 +61,10 @@ function initRunDir(workspaceRoot, runId) {
56
61
  // a migration named `CON` would otherwise produce a `CON.json` that the
57
62
  // agent can't write to.
58
63
  const WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
59
- /**
60
- * The bare `.` / `..` check must come first — otherwise a malformed migration
61
- * name of exactly `..` would let the handoff write escape the run directory.
62
- */
63
64
  function sanitizeSegment(value) {
64
65
  if (value === '.' || value === '..')
65
66
  return '_';
66
- let sanitized = value.replace(/[\x00-\x1f<>:"/\\|?*]/g, '_');
67
+ let sanitized = value.replace(/[\x00-\x1f<>:"/\\|?*+]/g, '_');
67
68
  // Windows forbids trailing dots/spaces on file/directory names.
68
69
  sanitized = sanitized.replace(/[. ]+$/, '');
69
70
  if (WINDOWS_RESERVED_NAMES.test(sanitized)) {
@@ -71,29 +72,111 @@ function sanitizeSegment(value) {
71
72
  }
72
73
  return sanitized || '_';
73
74
  }
75
+ const STEP_NAME_PREFIX_MAX_BYTES = 64;
76
+ // Cuts on code points so a multibyte character is never split, and counts
77
+ // UTF-8 bytes because the filesystem limit is per byte, not per character.
78
+ function truncateUtf8(value, maxBytes) {
79
+ let out = '';
80
+ let used = 0;
81
+ for (const ch of value) {
82
+ const bytes = Buffer.byteLength(ch);
83
+ if (used + bytes > maxBytes)
84
+ break;
85
+ out += ch;
86
+ used += bytes;
87
+ }
88
+ return out;
89
+ }
74
90
  /**
75
- * Absolute path of the handoff file for a migration step within a run, under
76
- * the run directory's `handoffs/` subtree: the pre-authorized write scope
77
- * stops there, so a handoff placed anywhere else costs an approval prompt.
78
- * The package's scope (if any) becomes a real subdirectory so the package name
79
- * stays readable; two packages can ship a migration with the same name without
80
- * colliding because they land in different package subdirectories. Each
81
- * segment is sanitized so the path is always writable on every platform.
91
+ * The name a step's own files are keyed by, bounded to fit one path component.
92
+ * Sanitizing folds many characters to `_` and the prefix is cut to fit, so the
93
+ * SHA-256 of the raw package and name is what keeps two migrations apart.
82
94
  */
95
+ function stepStem(migration) {
96
+ const prefix = truncateUtf8([...migration.package.split('/'), migration.name]
97
+ .map(sanitizeSegment)
98
+ .join('+'), STEP_NAME_PREFIX_MAX_BYTES);
99
+ const hash = (0, crypto_1.createHash)('sha256')
100
+ .update(JSON.stringify([migration.package, migration.name]))
101
+ .digest('hex');
102
+ return `${prefix}-${hash}`;
103
+ }
83
104
  function stepHandoffPath(runDir, migration) {
84
- return (0, path_1.join)(runDir, types_1.HANDOFFS_DIR_NAME, ...migration.package.split('/').map(sanitizeSegment), `${sanitizeSegment(migration.name)}.json`);
105
+ return (0, path_1.join)(runDir, types_1.HANDOFFS_DIR_NAME, `${stepStem(migration)}.json`);
106
+ }
107
+ /** Handoff path for a run step. No hash needed: step ids are unique within the run. */
108
+ function runStepHandoffPath(runDir, stepId) {
109
+ return (0, path_1.join)(runDir, types_1.HANDOFFS_DIR_NAME, `${sanitizeSegment(stepId)}.json`);
110
+ }
111
+ function stepPromptsDir(runDir, migration) {
112
+ return (0, path_1.join)(runDir, types_1.PROMPTS_DIR_NAME, stepStem(migration));
85
113
  }
86
114
  /**
87
- * Reads and validates a handoff file written by an agent. Returns a tagged
88
- * result so callers (the in-loop poller and the post-exit resolver) can
89
- * distinguish "file not yet written" from "file written but garbage" — the
90
- * latter is surfaced to the user instead of being collapsed into the same
91
- * generic ambiguous-outcome prompt.
115
+ * `lstat`, not `stat`: a symlink in the handoffs dir's place would send every
116
+ * handoff read and removal wherever it points.
92
117
  */
93
- function readHandoffWithReason(filePath) {
118
+ function handoffsDirState(handoffsDir) {
119
+ try {
120
+ return (0, fs_1.lstatSync)(handoffsDir).isDirectory() ? 'directory' : 'other';
121
+ }
122
+ catch (err) {
123
+ if (err?.code === 'ENOENT')
124
+ return 'missing';
125
+ throw err;
126
+ }
127
+ }
128
+ /**
129
+ * Reads the file `stat` describes, refusing a symlink swapped in after the
130
+ * caller's lstat: O_NOFOLLOW fails the open with ELOOP, and O_NONBLOCK keeps a
131
+ * planted FIFO from blocking it. Windows has neither flag, so there the inode
132
+ * comparison is what catches a followed symlink. It does not guard against an
133
+ * unlink and recreate reusing the inode number. Read errors propagate: a file
134
+ * the agent cannot read must not pass.
135
+ */
136
+ function readInspectedFile(filePath, stat, replacedMessage) {
137
+ const fd = (0, fs_1.openSync)(filePath, fs_1.constants.O_RDONLY |
138
+ (fs_1.constants.O_NOFOLLOW ?? 0) |
139
+ (fs_1.constants.O_NONBLOCK ?? 0));
140
+ try {
141
+ const fdStat = (0, fs_1.fstatSync)(fd, { bigint: true });
142
+ if (!fdStat.isFile() ||
143
+ fdStat.dev !== stat.dev ||
144
+ fdStat.ino !== stat.ino) {
145
+ throw new Error(replacedMessage);
146
+ }
147
+ return (0, fs_1.readFileSync)(fd, 'utf-8');
148
+ }
149
+ finally {
150
+ (0, fs_1.closeSync)(fd);
151
+ }
152
+ }
153
+ /**
154
+ * Splits "not written yet" from "written but garbage" so callers can surface a
155
+ * malformed handoff instead of collapsing it into the generic
156
+ * ambiguous-outcome prompt.
157
+ */
158
+ function readHandoffWithReason(filePath, handoffsDir) {
94
159
  let raw;
95
160
  try {
96
- raw = (0, fs_1.readFileSync)(filePath, 'utf-8');
161
+ switch (handoffsDirState(handoffsDir)) {
162
+ case 'missing':
163
+ return { ok: false, reason: 'missing' };
164
+ case 'other':
165
+ return {
166
+ ok: false,
167
+ reason: 'read-error',
168
+ detail: `${handoffsDir} is not a directory`,
169
+ };
170
+ }
171
+ const stat = (0, fs_1.lstatSync)(filePath, { bigint: true });
172
+ if (!stat.isFile()) {
173
+ return {
174
+ ok: false,
175
+ reason: 'read-error',
176
+ detail: `${filePath} is not a regular file`,
177
+ };
178
+ }
179
+ raw = readInspectedFile(filePath, stat, `${filePath} was replaced while being read`);
97
180
  }
98
181
  catch (err) {
99
182
  const code = err?.code;
@@ -148,8 +231,8 @@ function readHandoffWithReason(filePath) {
148
231
  * Used by the polling loop (`waitForValidHandoff`) where every failure mode
149
232
  * is "keep waiting" — the file may be missing, mid-write, or being rewritten.
150
233
  */
151
- function readHandoff(filePath) {
152
- const result = readHandoffWithReason(filePath);
234
+ function readHandoff(filePath, handoffsDir) {
235
+ const result = readHandoffWithReason(filePath, handoffsDir);
153
236
  return result.ok ? result.handoff : null;
154
237
  }
155
238
  /**
@@ -160,7 +243,7 @@ function readHandoff(filePath) {
160
243
  *
161
244
  * Rejects with the abort reason when `options.signal` is aborted.
162
245
  */
163
- function waitForValidHandoff(handoffFilePath, options = {}) {
246
+ function waitForValidHandoff(handoffFilePath, handoffsDir, options = {}) {
164
247
  const intervalMs = options.intervalMs ?? 500;
165
248
  const { signal } = options;
166
249
  return new Promise((resolve, reject) => {
@@ -180,7 +263,7 @@ function waitForValidHandoff(handoffFilePath, options = {}) {
180
263
  onAbort();
181
264
  return;
182
265
  }
183
- if (readHandoff(handoffFilePath) !== null) {
266
+ if (readHandoff(handoffFilePath, handoffsDir) !== null) {
184
267
  signal?.removeEventListener('abort', onAbort);
185
268
  resolve();
186
269
  return;
@@ -0,0 +1,21 @@
1
+ export interface StepInstructionFiles {
2
+ /** Absolute path: config loaders resolve it, not the agent from its cwd. */
3
+ systemPromptFilePath: string;
4
+ /** Single-line command-line text pointing the agent at its instructions. */
5
+ instructionsPointer: string;
6
+ }
7
+ export interface WriteStepInstructionFilesArgs {
8
+ workspaceRoot: string;
9
+ runDir: string;
10
+ migration: {
11
+ package: string;
12
+ name: string;
13
+ };
14
+ systemPrompt: string;
15
+ instructions: string;
16
+ }
17
+ /**
18
+ * Writes prompts as files on every platform to keep one delivery path.
19
+ * Windows npm shims cannot carry multi-line prompts within cmd.exe's limit.
20
+ */
21
+ export declare function writeStepInstructionFiles(args: WriteStepInstructionFilesArgs): StepInstructionFiles;