nx 23.3.0-beta.2 → 23.3.0-beta.4
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.
- package/dist/bin/init-local.js +7 -1
- package/dist/src/command-line/add/add.js +5 -1
- package/dist/src/command-line/init/implementation/utils.js +6 -1
- package/dist/src/command-line/migrate/agentic/capture-generator-output.d.ts +25 -0
- package/dist/src/command-line/migrate/agentic/capture-generator-output.js +121 -5
- package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +21 -0
- package/dist/src/command-line/migrate/agentic/close-agent-session.js +126 -0
- package/dist/src/command-line/migrate/agentic/definitions.d.ts +24 -0
- package/dist/src/command-line/migrate/agentic/definitions.js +5 -2
- package/dist/src/command-line/migrate/agentic/handoff.d.ts +23 -0
- package/dist/src/command-line/migrate/agentic/handoff.js +62 -3
- package/dist/src/command-line/migrate/agentic/master/invocations.d.ts +12 -0
- package/dist/src/command-line/migrate/agentic/master/invocations.js +54 -0
- package/dist/src/command-line/migrate/agentic/master/run-master-session.d.ts +12 -0
- package/dist/src/command-line/migrate/agentic/master/run-master-session.js +99 -0
- package/dist/src/command-line/migrate/agentic/master/spawn-master.d.ts +31 -0
- package/dist/src/command-line/migrate/agentic/master/spawn-master.js +217 -0
- package/dist/src/command-line/migrate/agentic/prompts/fragments.js +3 -0
- package/dist/src/command-line/migrate/agentic/runner.d.ts +0 -21
- package/dist/src/command-line/migrate/agentic/runner.js +12 -249
- package/dist/src/command-line/migrate/agentic/terminal-repair.d.ts +1 -0
- package/dist/src/command-line/migrate/agentic/terminal-repair.js +27 -0
- package/dist/src/command-line/migrate/agentic/windows-cmd.d.ts +33 -0
- package/dist/src/command-line/migrate/agentic/windows-cmd.js +88 -0
- package/dist/src/command-line/migrate/deferred-output.d.ts +44 -0
- package/dist/src/command-line/migrate/deferred-output.js +92 -0
- package/dist/src/command-line/migrate/execute-migration.d.ts +9 -3
- package/dist/src/command-line/migrate/execute-migration.js +53 -16
- package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -4
- package/dist/src/command-line/migrate/migrate-commits.js +7 -6
- package/dist/src/command-line/migrate/migrate.js +28 -13
- package/dist/src/command-line/migrate/run/broker.d.ts +138 -0
- package/dist/src/command-line/migrate/run/broker.js +507 -0
- package/dist/src/command-line/migrate/run/clean-retry.d.ts +12 -0
- package/dist/src/command-line/migrate/run/clean-retry.js +98 -0
- package/dist/src/command-line/migrate/run/index.d.ts +4 -3
- package/dist/src/command-line/migrate/run/index.js +7 -1
- package/dist/src/command-line/migrate/run/issues.d.ts +8 -1
- package/dist/src/command-line/migrate/run/issues.js +40 -6
- package/dist/src/command-line/migrate/run/orchestrator.d.ts +18 -1
- package/dist/src/command-line/migrate/run/orchestrator.js +409 -377
- package/dist/src/command-line/migrate/run/run-state.d.ts +16 -0
- package/dist/src/command-line/migrate/run/run-state.js +33 -2
- package/dist/src/command-line/migrate/run/runbook.js +8 -4
- package/dist/src/command-line/migrate/run/state-machine.d.ts +24 -1
- package/dist/src/command-line/migrate/run/state-machine.js +112 -21
- package/dist/src/command-line/migrate/run/util.d.ts +2 -1
- package/dist/src/command-line/migrate/run/util.js +3 -3
- package/dist/src/command-line/migrate/run/worker.js +170 -164
- package/dist/src/core/graph/main.js +1 -1
- package/dist/src/daemon/server/server.js +11 -8
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/tasks-runner/run-command.js +3 -0
- package/dist/src/utils/git-utils.d.ts +12 -0
- package/dist/src/utils/git-utils.js +76 -30
- package/dist/src/utils/package-json.js +8 -0
- package/package.json +11 -11
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runOrchestratorInit = runOrchestratorInit;
|
|
4
4
|
exports.runOrchestratorReconcile = runOrchestratorReconcile;
|
|
5
|
+
exports.completionWarnings = completionWarnings;
|
|
5
6
|
const child_process_1 = require("child_process");
|
|
6
7
|
const crypto_1 = require("crypto");
|
|
7
8
|
const fs_1 = require("fs");
|
|
8
9
|
const path_1 = require("path");
|
|
9
10
|
const fileutils_1 = require("../../../utils/fileutils");
|
|
10
11
|
const atomic_write_1 = require("./atomic-write");
|
|
12
|
+
const clean_retry_1 = require("./clean-retry");
|
|
13
|
+
const broker_1 = require("./broker");
|
|
11
14
|
const git_utils_1 = require("../../../utils/git-utils");
|
|
12
15
|
const versions_1 = require("../../../utils/versions");
|
|
13
16
|
const handoff_1 = require("../agentic/handoff");
|
|
@@ -81,7 +84,7 @@ function refuseUnsafeScratchExposure(exposure, thenWhat) {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
async function runOrchestratorInit(input) {
|
|
84
|
-
const { root, migrationsJson, createCommits, commitPrefix, skipInstall, installedNxVersion, validate, } = input;
|
|
87
|
+
const { root, migrationsJson, createCommits, commitPrefix, skipInstall, installedNxVersion, validate, emitAgentInstructions = true, } = input;
|
|
85
88
|
const planHash = (0, run_id_1.computePlanHash)(migrationsJson);
|
|
86
89
|
// An active run means a prior init already happened (e.g. it crashed before
|
|
87
90
|
// the agent's first reconcile); starting a second run would compete with it.
|
|
@@ -102,9 +105,9 @@ async function runOrchestratorInit(input) {
|
|
|
102
105
|
throw new Error(`The migration id '${id}' contains characters that are not shell-safe. Orchestrated runs require shell-safe migration ids.`);
|
|
103
106
|
}
|
|
104
107
|
}
|
|
108
|
+
const policy = { createCommits, skipInstall };
|
|
105
109
|
if (active) {
|
|
106
|
-
resumeRun(root, active.runId, active.state);
|
|
107
|
-
return;
|
|
110
|
+
return resumeRun(root, active.runId, active.state, policy, emitAgentInstructions);
|
|
108
111
|
}
|
|
109
112
|
const runId = (0, run_id_1.createRunId)();
|
|
110
113
|
const dir = (0, run_state_1.runDir)(root, runId);
|
|
@@ -200,10 +203,9 @@ async function runOrchestratorInit(input) {
|
|
|
200
203
|
return null;
|
|
201
204
|
});
|
|
202
205
|
if (winner) {
|
|
203
|
-
resumeRun(root, winner.runId, winner.state);
|
|
204
|
-
return;
|
|
206
|
+
return resumeRun(root, winner.runId, winner.state, policy, emitAgentInstructions);
|
|
205
207
|
}
|
|
206
|
-
finishInit(root, dir, runId, state, 'created');
|
|
208
|
+
return finishInit(root, dir, runId, state, 'created', emitAgentInstructions);
|
|
207
209
|
}
|
|
208
210
|
// Reads the newest active run, refusing one whose plan differs from the
|
|
209
211
|
// incoming plan; null when no run is active. Uninterpretable run dirs refuse
|
|
@@ -240,7 +242,14 @@ function findActiveRunForPlan(root, planHash) {
|
|
|
240
242
|
}
|
|
241
243
|
// Shared resume tail for an active run found before or under the creation
|
|
242
244
|
// lock, so the two discovery points cannot drift apart.
|
|
243
|
-
function resumeRun(root, runId, state) {
|
|
245
|
+
function resumeRun(root, runId, state, policy, emitAgentInstructions) {
|
|
246
|
+
// Refused before the checkpoint retry below, which commits.
|
|
247
|
+
if (state.createCommits !== policy.createCommits ||
|
|
248
|
+
(state.skipInstall === true) !== policy.skipInstall) {
|
|
249
|
+
throw new Error(`Nx did not resume the active migrate run because its recorded install and commit policy differs from this invocation's. ` +
|
|
250
|
+
`Nx takes that policy from --create-commits and --skip-install, and from nx.json "migrate.createCommits" when the commit flag is omitted. ` +
|
|
251
|
+
`Re-run with explicit flags for the policy run '${runId}' started with, or remove ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId} to abandon it.`);
|
|
252
|
+
}
|
|
244
253
|
const dir = (0, run_state_1.runDir)(root, runId);
|
|
245
254
|
// Ignore/index state can change while a durable run is paused (a checkout,
|
|
246
255
|
// a .gitignore edit, a forced add). Probe before the checkpoint retry:
|
|
@@ -254,13 +263,13 @@ function resumeRun(root, runId, state) {
|
|
|
254
263
|
// contract must not first change git history or durable run state.
|
|
255
264
|
const runbook = ensureRunbook(root, dir, runId, state);
|
|
256
265
|
if (runbook === null) {
|
|
257
|
-
return;
|
|
266
|
+
return { kind: 'refused' };
|
|
258
267
|
}
|
|
259
268
|
// A run flagged checkpointFailed gets one more chance to capture the
|
|
260
269
|
// pre-existing tree state before its first migration commit absorbs it.
|
|
261
270
|
const resumed = ensureCheckpoint(root, dir, state);
|
|
262
271
|
announceResume(runId, resumed);
|
|
263
|
-
finishInit(root, dir, runId, resumed, 'resumed', runbook);
|
|
272
|
+
return finishInit(root, dir, runId, resumed, 'resumed', emitAgentInstructions, runbook);
|
|
264
273
|
}
|
|
265
274
|
function announceResume(runId, state) {
|
|
266
275
|
const applied = state.steps.filter((s) => s.status === 'succeeded').length;
|
|
@@ -289,26 +298,43 @@ function ensureCheckpoint(root, dir, state) {
|
|
|
289
298
|
return state;
|
|
290
299
|
if (state.steps.some((s) => s.status !== 'pending'))
|
|
291
300
|
return state;
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
301
|
+
// Reserved so no step is dispensed into the tree the checkpoint captures;
|
|
302
|
+
// a run that moved on since the snapshot has nothing left to capture.
|
|
303
|
+
let lease;
|
|
304
|
+
try {
|
|
305
|
+
lease = (0, broker_1.acquireTreeOperation)(dir, { kind: 'checkpoint' });
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
if (e instanceof broker_1.BrokerStaleRequestError)
|
|
309
|
+
return state;
|
|
310
|
+
throw e;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
// The checkpoint commit is a git side effect, so it runs before the lock;
|
|
314
|
+
// the ledger append and flag clear then apply to the fresh on-disk state.
|
|
315
|
+
const checkpoint = checkpointEntry(root, state.commitPrefix);
|
|
316
|
+
// The retried checkpoint captured everything, so clean retries are safe
|
|
317
|
+
// again. Only a verified-clean tree clears the flag: a failed probe proves
|
|
318
|
+
// nothing was captured.
|
|
319
|
+
const cleared = (0, git_utils_1.getWorkingTreeStatus)(root) === 'clean';
|
|
320
|
+
if (!checkpoint && !cleared)
|
|
321
|
+
return state;
|
|
322
|
+
return (0, state_lock_1.updateRunState)(dir, (fresh) => {
|
|
323
|
+
// Re-check both guards on the fresh state: a concurrent reconcile may
|
|
324
|
+
// have cleared the flag or advanced a step while the commit ran.
|
|
325
|
+
// Skipping here can leave that commit unledgered, the documented
|
|
326
|
+
// crash-window shape.
|
|
327
|
+
if (!fresh.checkpointFailed ||
|
|
328
|
+
fresh.steps.some((s) => s.status !== 'pending')) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const next = checkpoint ? (0, state_machine_1.appendCommit)(fresh, checkpoint) : fresh;
|
|
332
|
+
return cleared ? { ...next, checkpointFailed: false } : next;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
lease.release();
|
|
337
|
+
}
|
|
312
338
|
}
|
|
313
339
|
// Commits pre-existing working-tree state so the first migration's commit can't
|
|
314
340
|
// absorb it, returning the ledger entry only when a commit verifiably landed.
|
|
@@ -331,7 +357,7 @@ function checkpointEntry(root, commitPrefix) {
|
|
|
331
357
|
// No migration step is dispensed here: the agent reads the runbook first and
|
|
332
358
|
// asks for the run's current step by reconciling, so the contract always lands
|
|
333
359
|
// before the first command does.
|
|
334
|
-
function finishInit(root, dir, runId, state, origin,
|
|
360
|
+
function finishInit(root, dir, runId, state, origin, emitAgentInstructions,
|
|
335
361
|
// The runbook bytes when the caller already ensured them (the resume path,
|
|
336
362
|
// which must fail before its git and state side effects).
|
|
337
363
|
runbook) {
|
|
@@ -358,7 +384,17 @@ runbook) {
|
|
|
358
384
|
}
|
|
359
385
|
const content = runbook ?? ensureRunbook(root, dir, runId, current);
|
|
360
386
|
if (content === null) {
|
|
361
|
-
return;
|
|
387
|
+
return { kind: 'refused' };
|
|
388
|
+
}
|
|
389
|
+
const ready = {
|
|
390
|
+
kind: 'ready',
|
|
391
|
+
runId,
|
|
392
|
+
runRoot: root,
|
|
393
|
+
runbookPath: (0, path_1.join)(dir, current.runbookPath ?? runbook_1.RUNBOOK_FILE_NAME),
|
|
394
|
+
reconcileCommand: reconcileCommand(root, runId),
|
|
395
|
+
};
|
|
396
|
+
if (!emitAgentInstructions) {
|
|
397
|
+
return ready;
|
|
362
398
|
}
|
|
363
399
|
(0, agent_output_1.emitRunbookBlock)(runId, content);
|
|
364
400
|
const instructionLines = [
|
|
@@ -369,9 +405,10 @@ runbook) {
|
|
|
369
405
|
const lines = (0, agent_output_1.safeLines)(instructionLines);
|
|
370
406
|
(0, agent_output_1.logToAgent)({ title: `nx migrate: run ${origin}`, bodyLines: lines });
|
|
371
407
|
(0, agent_output_1.emitStepBlock)(runId, '-', 'initialized', {
|
|
372
|
-
next: reconcileCommand
|
|
408
|
+
next: ready.reconcileCommand,
|
|
373
409
|
instructions: lines.join('\n'),
|
|
374
410
|
});
|
|
411
|
+
return ready;
|
|
375
412
|
}
|
|
376
413
|
// A missing runbook is re-rendered only by the nx version that created the
|
|
377
414
|
// run: its content is version-locked, and a different nx re-rendering it would
|
|
@@ -484,128 +521,100 @@ async function runOrchestratorReconcile(input) {
|
|
|
484
521
|
state = detectDeaths(dir, state);
|
|
485
522
|
// (c) apply the decision relay to the single failed/died step.
|
|
486
523
|
if (stepAction) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
entry: null,
|
|
513
|
-
|
|
524
|
+
// Owns the tree reservation a clean retry's reset, an adopt's commit or a
|
|
525
|
+
// skip's install takes, released once the transition that records it is
|
|
526
|
+
// written.
|
|
527
|
+
const scope = {};
|
|
528
|
+
try {
|
|
529
|
+
const result = await applyReconcileStepAction(root, dir, state, stepAction, scope);
|
|
530
|
+
if (result.kind === 'error') {
|
|
531
|
+
emitError(root, runId, result.reason);
|
|
532
|
+
return; // no transition was written
|
|
533
|
+
}
|
|
534
|
+
const target = result.targetStep;
|
|
535
|
+
// The commit is a git side effect, so it runs before the lock (locked
|
|
536
|
+
// sections stay synchronous); the transition and any unrecorded entry then
|
|
537
|
+
// land in one write. Adopt and skip keep the tree, so the install they owe
|
|
538
|
+
// runs here: the next dispense would take the changed deps as its baseline.
|
|
539
|
+
// Retries owe nothing: the rearmed attempt reconciles itself.
|
|
540
|
+
const { entry, installFailed, recorded } = stepAction === 'adopt'
|
|
541
|
+
? state.createCommits
|
|
542
|
+
? await commitForStep(root, dir, state, target, scope)
|
|
543
|
+
: {
|
|
544
|
+
entry: null,
|
|
545
|
+
installFailed: await installFailedForStep(root, dir, state, target, 'action-install', scope),
|
|
546
|
+
}
|
|
547
|
+
: stepAction === 'skip'
|
|
548
|
+
? await retainedTreeSideEffects(root, dir, state, target, 'action-install', scope)
|
|
549
|
+
: { entry: null, installFailed: false };
|
|
550
|
+
// A rearm starts a fresh attempt; drop the stale handoff before the rearm
|
|
551
|
+
// is persisted so a crash in between can't refold the old outcome into the
|
|
552
|
+
// new attempt. Losing the handoff without the rearm is safe: the step is
|
|
553
|
+
// still failed/died and the agent re-issues the action.
|
|
554
|
+
if (stepAction === 'retry' || stepAction === 'retry-clean') {
|
|
555
|
+
removeHandoff(dir, target.id);
|
|
556
|
+
// A reset-backed retry reruns the generator, so payloads from earlier
|
|
557
|
+
// attempts describe a tree that was reset away. Hygiene only: the persisted
|
|
558
|
+
// generatorCompletedAtAttempt bound is the correctness gate.
|
|
559
|
+
if (stepAction === 'retry-clean' &&
|
|
560
|
+
result.state.steps.find((s) => s.id === target.id)
|
|
561
|
+
.generatorCompleted !== true) {
|
|
562
|
+
(0, agent_work_payload_1.removeAgentWorkPayloads)(dir, target.id, target.attempt);
|
|
514
563
|
}
|
|
515
|
-
: stepAction === 'skip'
|
|
516
|
-
? await retainedTreeSideEffects(root, dir, state, target)
|
|
517
|
-
: { entry: null, installFailed: false };
|
|
518
|
-
// A rearm starts a fresh attempt; drop the stale handoff before the rearm
|
|
519
|
-
// is persisted so a crash in between can't refold the old outcome into the
|
|
520
|
-
// new attempt. Losing the handoff without the rearm is safe: the step is
|
|
521
|
-
// still failed/died and the agent re-issues the action.
|
|
522
|
-
if (stepAction === 'retry' || stepAction === 'retry-clean') {
|
|
523
|
-
removeHandoff(dir, target.id);
|
|
524
|
-
// A reset-backed retry that dropped the generator marker reruns the
|
|
525
|
-
// generator, so payloads stored by earlier attempts describe a run
|
|
526
|
-
// whose tree was reset away; remove them. Hygiene, not the correctness
|
|
527
|
-
// boundary: the lineage bound persisted with the next marker
|
|
528
|
-
// (generatorCompletedAtAttempt) is what keeps a later retry from
|
|
529
|
-
// re-handing a copy this best-effort removal missed. A plain retry
|
|
530
|
-
// keeps the files: its lineage is unbroken, and a retained retry
|
|
531
|
-
// re-hands the newest copy. Removed with the handoff, before the rearm
|
|
532
|
-
// is persisted: losing them without the rearm only costs a later
|
|
533
|
-
// emission its stored copy.
|
|
534
|
-
if (stepAction === 'retry-clean' &&
|
|
535
|
-
result.state.steps.find((s) => s.id === target.id)
|
|
536
|
-
.generatorCompleted !== true) {
|
|
537
|
-
(0, agent_work_payload_1.removeAgentWorkPayloads)(dir, target.id, target.attempt);
|
|
538
564
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
attempt
|
|
565
|
+
// Re-validate against fresh disk state so a concurrent reconcile's own
|
|
566
|
+
// rejection surfaces through emitError instead of being written over. The
|
|
567
|
+
// bound attempt keeps the snapshot checks above honest.
|
|
568
|
+
let freshRejection;
|
|
569
|
+
const written = (0, state_lock_1.updateRunState)(dir, (fresh) => {
|
|
570
|
+
// A plain retry takes no reservation of its own, so this is where it
|
|
571
|
+
// learns that a live process still commits or installs for the step.
|
|
572
|
+
const held = (0, broker_1.liveTreeOperation)(fresh, scope.lease?.owner);
|
|
573
|
+
if (held) {
|
|
574
|
+
freshRejection = (0, broker_1.treeBusyMessage)(held);
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
// A plain retry's acceptance read the generator marker on the
|
|
578
|
+
// snapshot; a clean retry that started meanwhile forgets the marker
|
|
579
|
+
// without moving the attempt, so the attempt check cannot see it.
|
|
580
|
+
const freshStep = fresh.steps.find((s) => s.id === target.id);
|
|
581
|
+
if (stepAction === 'retry' &&
|
|
582
|
+
freshStep !== undefined &&
|
|
583
|
+
generatorPending(freshStep) !== generatorPending(target)) {
|
|
584
|
+
freshRejection = `Cannot apply action 'retry' to step '${target.id}': whether its generator ran changed since this reconcile read it. Run the reconcile again.`;
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const reapplied = (0, state_machine_1.applyStepEvent)(fresh, {
|
|
588
|
+
type: 'stepAction',
|
|
589
|
+
stepId: target.id,
|
|
590
|
+
action: stepAction,
|
|
591
|
+
attempt: target.attempt,
|
|
592
|
+
});
|
|
593
|
+
if (reapplied.kind === 'error') {
|
|
594
|
+
freshRejection = reapplied.reason;
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const next = installFailed
|
|
598
|
+
? (0, state_machine_1.markInstallFailed)(reapplied.state, target.id)
|
|
599
|
+
: reapplied.state;
|
|
600
|
+
// An adopted commit absorbs uncovered failed steps the same way a fold
|
|
601
|
+
// commit does, so it carries their resolved issues too. A session's
|
|
602
|
+
// parent records its own commits as it answers.
|
|
603
|
+
return entry && !recorded
|
|
604
|
+
? (0, state_machine_1.appendCommit)(next, (0, issues_1.attachIssueIdsToCommitEntry)(next, entry))
|
|
605
|
+
: next;
|
|
554
606
|
});
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
// so resolutions that attempt claimed and no landed commit carries are
|
|
561
|
-
// reverted with the rearm, in the same write.
|
|
562
|
-
let rearmed = reapplied.state;
|
|
563
|
-
if (stepAction === 'retry-clean') {
|
|
564
|
-
const reopened = (0, issues_1.reopenResolutionsForStep)(rearmed, target.id);
|
|
565
|
-
rearmed = reopened.state;
|
|
566
|
-
reopenedIssueUpdates = reopened.updates;
|
|
607
|
+
if (freshRejection) {
|
|
608
|
+
emitError(root, runId, entry?.kind === 'landed' && entry.sha
|
|
609
|
+
? `${freshRejection} Note: this action's commit ${entry.sha} had already landed and stays in history; resolve the step against the tree as it stands now.`
|
|
610
|
+
: freshRejection);
|
|
611
|
+
return;
|
|
567
612
|
}
|
|
568
|
-
|
|
569
|
-
? (0, state_machine_1.markInstallFailed)(rearmed, target.id)
|
|
570
|
-
: rearmed;
|
|
571
|
-
// An adopted commit absorbs uncovered failed steps the same way a fold
|
|
572
|
-
// commit does, so it carries their resolved issues too.
|
|
573
|
-
return entry
|
|
574
|
-
? appendCommit(next, (0, issues_1.attachIssueIdsToCommitEntry)(next, entry))
|
|
575
|
-
: next;
|
|
576
|
-
});
|
|
577
|
-
if (freshRejection) {
|
|
578
|
-
emitError(root, runId, entry?.kind === 'landed' && entry.sha
|
|
579
|
-
? `${freshRejection} Note: this action's commit ${entry.sha} had already landed and stays in history; resolve the step against the tree as it stands now.`
|
|
580
|
-
: freshRejection);
|
|
581
|
-
return;
|
|
613
|
+
state = written;
|
|
582
614
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
// authoritative, so a failed append loses only the archive's trail record
|
|
586
|
-
// of the revert. The sink survives a throw: a shell rebuilt before the
|
|
587
|
-
// failure is durable and reads healthy on retry, so this pass must warn
|
|
588
|
-
// it.
|
|
589
|
-
const revertApplication = {
|
|
590
|
-
state: written,
|
|
591
|
-
newIssues: [],
|
|
592
|
-
updates: reopenedIssueUpdates,
|
|
593
|
-
};
|
|
594
|
-
const revertReconstructedIds = [];
|
|
595
|
-
try {
|
|
596
|
-
(0, issues_1.archiveIssues)(dir, revertApplication, revertReconstructedIds);
|
|
597
|
-
}
|
|
598
|
-
catch (e) {
|
|
599
|
-
(0, agent_output_1.warnToAgent)({
|
|
600
|
-
title: `The reverted issue resolutions for ${target.migrationId} could not be archived (${(0, util_1.summarizeError)(e)}).`,
|
|
601
|
-
bodyLines: [
|
|
602
|
-
`run.json stays authoritative for the dispositions; the archived files under the run's issues directory miss the revert records, so their last entries may still read resolved.`,
|
|
603
|
-
],
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
warnReconstructedArchives(revertReconstructedIds);
|
|
615
|
+
finally {
|
|
616
|
+
scope.lease?.release();
|
|
607
617
|
}
|
|
608
|
-
state = written;
|
|
609
618
|
}
|
|
610
619
|
// (d) choose and emit the next dispense.
|
|
611
620
|
advanceAndDispense(root, dir, runId);
|
|
@@ -621,17 +630,6 @@ function buildSteps(sortedMigrations) {
|
|
|
621
630
|
hasGenerator: !(0, migration_shape_1.isPromptOnlyMigration)(m),
|
|
622
631
|
}));
|
|
623
632
|
}
|
|
624
|
-
// --- reconcile phases -------------------------------------------------------
|
|
625
|
-
// The runbook points every later consumer at issues/<id>.json for the full
|
|
626
|
-
// details, so a rebuild from run-state fields (the reported detail is gone
|
|
627
|
-
// with the lost file) must not stay silent.
|
|
628
|
-
function warnReconstructedArchives(issueIds) {
|
|
629
|
-
if (issueIds.length === 0)
|
|
630
|
-
return;
|
|
631
|
-
(0, agent_output_1.warnToAgent)({
|
|
632
|
-
title: `The archived details for ${issueIds.join(', ')} were missing or unreadable and were rebuilt from the run state; the originally reported detail is lost.`,
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
633
|
async function foldHandoffs(root, dir, state) {
|
|
636
634
|
let current = state;
|
|
637
635
|
// Step ids are fixed for the life of a run, so the ids come from the caller's
|
|
@@ -668,6 +666,9 @@ async function foldHandoffs(root, dir, state) {
|
|
|
668
666
|
});
|
|
669
667
|
if (applied.kind === 'error')
|
|
670
668
|
return;
|
|
669
|
+
// A corrupt receipt refuses the fold here, before the commit below
|
|
670
|
+
// could land unrecorded.
|
|
671
|
+
(0, state_machine_1.commitReceipt)(fresh, fresh.steps.find((s) => s.id === step.id));
|
|
671
672
|
const issues = (0, issues_1.parseHandoffIssues)(result.handoff.extras, fresh, step);
|
|
672
673
|
if (issues.ok !== true)
|
|
673
674
|
return;
|
|
@@ -683,7 +684,7 @@ async function foldHandoffs(root, dir, state) {
|
|
|
683
684
|
}
|
|
684
685
|
ready = true;
|
|
685
686
|
});
|
|
686
|
-
warnReconstructedArchives(reconstructedIssueIds);
|
|
687
|
+
(0, issues_1.warnReconstructedArchives)(reconstructedIssueIds);
|
|
687
688
|
if (archiveError !== null) {
|
|
688
689
|
(0, agent_output_1.warnToAgent)({
|
|
689
690
|
title: `The issue details reported by ${step.migrationId} could not be archived (${(0, util_1.summarizeError)(archiveError)}).`,
|
|
@@ -694,73 +695,85 @@ async function foldHandoffs(root, dir, state) {
|
|
|
694
695
|
}
|
|
695
696
|
if (!ready)
|
|
696
697
|
continue;
|
|
697
|
-
// Phase 2: the commit and the install are side effects, so they
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
// the step settled with its commit forgotten.
|
|
701
|
-
const { entry, installFailed } = await foldLedgerEntry(root, dir, current, step, promptOutcome);
|
|
702
|
-
// The write re-validates against fresh disk state, on the attempt this
|
|
703
|
-
// handoff was read for. That window is wide (a git commit plus a package
|
|
704
|
-
// install), and 'awaiting-prompt-outcome' recurs, so without the attempt
|
|
705
|
-
// check a concurrent reconcile's retry could take this outcome as its own.
|
|
706
|
-
// A dropped fold is equivalent to the crash-refold window: the commit
|
|
707
|
-
// landed but the ledger misses it.
|
|
708
|
-
// Written through the lock directly so the issue application is re-archived
|
|
709
|
-
// on the state the write actually lands on: a claim assigned between the
|
|
710
|
-
// phases can add an update record phase 1 never saw.
|
|
698
|
+
// Phase 2: the commit and the install are side effects, so they run outside
|
|
699
|
+
// the lock; the transition, the issue application and any unrecorded ledger
|
|
700
|
+
// entry then land in one fresh-state write.
|
|
711
701
|
let folded = false;
|
|
712
702
|
let updateArchiveError = null;
|
|
713
703
|
let detailArchiveError = null;
|
|
714
704
|
let archivesDegraded = false;
|
|
715
705
|
const refoldReconstructedIds = [];
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const issues = (0, issues_1.parseHandoffIssues)(result.handoff.extras, fresh, step);
|
|
727
|
-
if (issues.ok !== true)
|
|
728
|
-
return fresh;
|
|
729
|
-
const application = (0, issues_1.applyReportedIssues)(applied.state, step, issues.issues, issues.updates);
|
|
730
|
-
try {
|
|
731
|
-
// Phase 1 wrote these files, so a reconstruction here means one
|
|
732
|
-
// vanished between the phases; the ids surface that loss. The sink
|
|
733
|
-
// survives a throw, so a shell rebuilt before a later batch failed
|
|
734
|
-
// still gets warned.
|
|
735
|
-
(0, issues_1.archiveIssues)(dir, application, refoldReconstructedIds);
|
|
736
|
-
}
|
|
737
|
-
catch (e) {
|
|
738
|
-
// A landed commit outranks the drop: refolding would re-attempt
|
|
739
|
-
// its commit against a clean tree as no-changes and lose the entry
|
|
740
|
-
// for good. Phase 1 already archived this handoff's records durably
|
|
741
|
-
// once; the fold proceeds and the loss is warned.
|
|
742
|
-
const intact = (0, issues_1.applicationArchivesIntact)(dir, application);
|
|
743
|
-
if (entry?.kind !== 'landed' && intact !== true) {
|
|
744
|
-
detailArchiveError = e;
|
|
706
|
+
const scope = {};
|
|
707
|
+
try {
|
|
708
|
+
const { entry, installFailed, recorded } = await foldLedgerEntry(root, dir, current, step, promptOutcome, scope);
|
|
709
|
+
// Bound to the attempt this handoff was read for: the window is wide (a
|
|
710
|
+
// commit plus an install) and 'awaiting-prompt-outcome' recurs, so a
|
|
711
|
+
// concurrent retry could otherwise take this outcome as its own. Under the
|
|
712
|
+
// lock so the issue application re-archives on the state it lands on.
|
|
713
|
+
current = (0, state_lock_1.withRunStateLock)(dir, () => {
|
|
714
|
+
const fresh = (0, run_state_1.readRunState)(dir);
|
|
715
|
+
if ((0, broker_1.liveTreeOperation)(fresh, scope.lease?.owner))
|
|
745
716
|
return fresh;
|
|
717
|
+
const applied = (0, state_machine_1.applyStepEvent)(fresh, {
|
|
718
|
+
type: 'foldPromptOutcome',
|
|
719
|
+
stepId: step.id,
|
|
720
|
+
attempt: step.attempt,
|
|
721
|
+
promptOutcome,
|
|
722
|
+
});
|
|
723
|
+
if (applied.kind === 'error')
|
|
724
|
+
return fresh;
|
|
725
|
+
const issues = (0, issues_1.parseHandoffIssues)(result.handoff.extras, fresh, step);
|
|
726
|
+
if (issues.ok !== true)
|
|
727
|
+
return fresh;
|
|
728
|
+
// A commit this process ran takes the handoff's resolutions when it is
|
|
729
|
+
// appended below; otherwise the entry already recorded for this attempt
|
|
730
|
+
// takes them, stamped at its index.
|
|
731
|
+
const receipt = (0, state_machine_1.commitReceipt)(applied.state, applied.state.steps.find((s) => s.id === step.id));
|
|
732
|
+
const carrier = entry && !recorded ? undefined : receipt;
|
|
733
|
+
const application = (0, issues_1.applyReportedIssues)(applied.state, step, issues.issues, issues.updates, carrier?.index);
|
|
734
|
+
try {
|
|
735
|
+
// Phase 1 wrote these files, so a reconstruction here means one
|
|
736
|
+
// vanished between the phases; the ids surface that loss. The sink
|
|
737
|
+
// survives a throw, so a shell rebuilt before a later batch failed
|
|
738
|
+
// still gets warned.
|
|
739
|
+
(0, issues_1.archiveIssues)(dir, application, refoldReconstructedIds);
|
|
746
740
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
741
|
+
catch (e) {
|
|
742
|
+
// A landed commit outranks the drop: refolding would re-attempt
|
|
743
|
+
// its commit against a clean tree as no-changes and lose the entry
|
|
744
|
+
// for good. Phase 1 already archived this handoff's records durably
|
|
745
|
+
// once; the fold proceeds and the loss is warned.
|
|
746
|
+
const intact = (0, issues_1.applicationArchivesIntact)(dir, application);
|
|
747
|
+
if (entry?.kind !== 'landed' && intact !== true) {
|
|
748
|
+
detailArchiveError = e;
|
|
749
|
+
return fresh;
|
|
750
|
+
}
|
|
751
|
+
updateArchiveError = e;
|
|
752
|
+
archivesDegraded = intact !== true;
|
|
753
|
+
}
|
|
754
|
+
folded = true;
|
|
755
|
+
let next = installFailed
|
|
756
|
+
? (0, state_machine_1.markInstallFailed)(application.state, step.id)
|
|
757
|
+
: application.state;
|
|
758
|
+
// A landed commit carries the fixes of every issue resolved by a step it
|
|
759
|
+
// names, absorbed steps included.
|
|
760
|
+
if (carrier)
|
|
761
|
+
next = (0, issues_1.enrichCommitEntryIssueIds)(next, carrier.index);
|
|
762
|
+
const written = entry && !recorded
|
|
763
|
+
? (0, state_machine_1.appendCommit)(next, (0, issues_1.attachIssueIdsToCommitEntry)(next, entry))
|
|
764
|
+
: next;
|
|
765
|
+
(0, run_state_1.writeRunState)(dir, written);
|
|
766
|
+
return written;
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
finally {
|
|
770
|
+
scope.lease?.release();
|
|
771
|
+
}
|
|
772
|
+
// The written state still names the lease just released; the caller's
|
|
773
|
+
// own reservation checks must not read it as another process's hold.
|
|
774
|
+
if (scope.lease)
|
|
775
|
+
current = (0, run_state_1.readRunState)(dir);
|
|
776
|
+
(0, issues_1.warnReconstructedArchives)(refoldReconstructedIds);
|
|
764
777
|
if (detailArchiveError !== null) {
|
|
765
778
|
(0, agent_output_1.warnToAgent)({
|
|
766
779
|
title: `The issue details reported by ${step.migrationId} could not be archived (${(0, util_1.summarizeError)(detailArchiveError)}).`,
|
|
@@ -797,54 +810,50 @@ async function foldHandoffs(root, dir, state) {
|
|
|
797
810
|
}
|
|
798
811
|
return current;
|
|
799
812
|
}
|
|
800
|
-
// What a folded prompt outcome owes the run state.
|
|
801
|
-
// commits on
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
|
|
806
|
-
// step's dispense captures the already-modified state as its own baseline.
|
|
807
|
-
//
|
|
808
|
-
// A failed or skipped prompt is not committed, but it can still have left
|
|
809
|
-
// edits behind, so a tree that is not verifiably clean records debt: the
|
|
810
|
-
// changes then read as pending for a later commit to absorb, and the
|
|
811
|
-
// completion warning knows about them. A failed probe counts as dirty,
|
|
812
|
-
// matching every other retry-safety decision in this file; debt a later landed
|
|
813
|
-
// entry covers costs nothing.
|
|
814
|
-
async function foldLedgerEntry(root, dir, state, step, promptOutcome) {
|
|
813
|
+
// What a folded prompt outcome owes the run state. Only a completed prompt
|
|
814
|
+
// with commits on rides its install in on the commit path; every other
|
|
815
|
+
// outcome installs here, or the next dispense takes the unreconciled
|
|
816
|
+
// package.json edit as its own baseline. A tree that is not verifiably clean
|
|
817
|
+
// records debt; a failed probe counts as dirty.
|
|
818
|
+
async function foldLedgerEntry(root, dir, state, step, promptOutcome, scope) {
|
|
815
819
|
if (promptOutcome.status === 'completed') {
|
|
816
820
|
if (state.createCommits) {
|
|
817
|
-
return commitForStep(root, dir, state, step);
|
|
821
|
+
return commitForStep(root, dir, state, step, scope);
|
|
818
822
|
}
|
|
819
823
|
return {
|
|
820
824
|
entry: null,
|
|
821
|
-
installFailed: await installFailedForStep(root, dir, state, step),
|
|
825
|
+
installFailed: await installFailedForStep(root, dir, state, step, 'fold-install', scope),
|
|
822
826
|
};
|
|
823
827
|
}
|
|
824
|
-
return retainedTreeSideEffects(root, dir, state, step);
|
|
828
|
+
return retainedTreeSideEffects(root, dir, state, step, 'fold-install', scope);
|
|
825
829
|
}
|
|
826
830
|
// Shared by prompts that did not complete and by skipped failed or died steps:
|
|
827
831
|
// the tree is kept as it stands, so the step still owes the install of any
|
|
828
832
|
// dependency edits it left and, with commits on, a debt record when the tree
|
|
829
833
|
// is not verifiably clean (see foldLedgerEntry for why).
|
|
830
|
-
async function retainedTreeSideEffects(root, dir, state, step) {
|
|
831
|
-
const installFailed = await installFailedForStep(root, dir, state, step);
|
|
834
|
+
async function retainedTreeSideEffects(root, dir, state, step, seam, scope) {
|
|
835
|
+
const installFailed = await installFailedForStep(root, dir, state, step, seam, scope);
|
|
832
836
|
const entry = state.createCommits && (0, git_utils_1.getWorkingTreeStatus)(root) !== 'clean'
|
|
833
837
|
? { kind: 'failed', stepIds: [step.id] }
|
|
834
838
|
: null;
|
|
835
839
|
return { entry, installFailed };
|
|
836
840
|
}
|
|
837
841
|
// Installs the dependency changes a step's tree may carry when no commit path
|
|
838
|
-
// will
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
|
|
842
|
-
|
|
842
|
+
// will, returning whether the install failed. A failure is recorded rather
|
|
843
|
+
// than thrown: reconcile still owes the agent a dispense. Broker errors do
|
|
844
|
+
// throw, as in commitForStep: the next reconcile redoes the action.
|
|
845
|
+
async function installFailedForStep(root, dir, state, step, seam, scope) {
|
|
846
|
+
const skipInstall = state.skipInstall === true;
|
|
843
847
|
try {
|
|
844
|
-
await (0, util_1.installDepsChangedSinceDispense)(root, dir, step,
|
|
848
|
+
await (0, broker_1.installStepTree)(dir, step, seam, () => (0, util_1.installDepsChangedSinceDispense)(root, dir, step, skipInstall, reconcileCommand(root, state.runId)), scope);
|
|
845
849
|
return false;
|
|
846
850
|
}
|
|
847
851
|
catch (e) {
|
|
852
|
+
if (e instanceof broker_1.BrokerStaleRequestError ||
|
|
853
|
+
e instanceof broker_1.BrokerUnavailableError ||
|
|
854
|
+
e instanceof broker_1.TreeBusyError) {
|
|
855
|
+
throw e;
|
|
856
|
+
}
|
|
848
857
|
(0, agent_output_1.warnToAgent)({
|
|
849
858
|
title: `The dependencies changed by ${step.migrationId} could not be installed (${(0, util_1.summarizeError)(e)}).`,
|
|
850
859
|
bodyLines: [`Run \`${(0, util_1.pmInstallCommand)(root)}\` before continuing.`],
|
|
@@ -878,6 +887,10 @@ function detectDeaths(dir, state) {
|
|
|
878
887
|
// snapshot and the write, or a retry already put a live worker on the
|
|
879
888
|
// step, the transition is rejected and the step is left as recorded.
|
|
880
889
|
current = (0, state_lock_1.updateRunState)(dir, (fresh) => {
|
|
890
|
+
// A dead worker's commit or install may still be running in the
|
|
891
|
+
// session's parent; the step stays running until that lands.
|
|
892
|
+
if ((0, broker_1.liveTreeOperation)(fresh))
|
|
893
|
+
return null;
|
|
881
894
|
const applied = (0, state_machine_1.applyStepEvent)(fresh, {
|
|
882
895
|
type: 'markDied',
|
|
883
896
|
stepId: step.id,
|
|
@@ -888,7 +901,7 @@ function detectDeaths(dir, state) {
|
|
|
888
901
|
}
|
|
889
902
|
return current;
|
|
890
903
|
}
|
|
891
|
-
function applyReconcileStepAction(root, state, action) {
|
|
904
|
+
async function applyReconcileStepAction(root, dir, state, action, scope) {
|
|
892
905
|
const candidates = state.steps.filter((s) => s.status === 'failed' || s.status === 'died');
|
|
893
906
|
if (candidates.length === 0) {
|
|
894
907
|
return {
|
|
@@ -903,29 +916,51 @@ function applyReconcileStepAction(root, state, action) {
|
|
|
903
916
|
};
|
|
904
917
|
}
|
|
905
918
|
const step = candidates[0];
|
|
919
|
+
const held = (0, broker_1.liveTreeOperation)(state);
|
|
920
|
+
if (held) {
|
|
921
|
+
return { kind: 'error', reason: (0, broker_1.treeBusyMessage)(held) };
|
|
922
|
+
}
|
|
906
923
|
// A retry-clean the dispense would not have offered must be refused here
|
|
907
924
|
// too, or a hand-crafted reconcile could reset a tree with no restore point
|
|
908
925
|
// and destroy prior steps' work.
|
|
909
926
|
if (action === 'retry-clean') {
|
|
910
927
|
const head = (0, git_utils_1.getLatestCommitSha)(root);
|
|
911
928
|
const fallback = step.status === 'died'
|
|
912
|
-
?
|
|
913
|
-
|
|
914
|
-
|
|
929
|
+
? (0, state_machine_1.commitMayBeInHistory)(state, step)
|
|
930
|
+
? `Use 'adopt' instead.`
|
|
931
|
+
: `Use 'adopt' or 'skip' instead.`
|
|
932
|
+
: (0, state_machine_1.commitMayBeInHistory)(state, step)
|
|
933
|
+
? `Use 'retry' instead.`
|
|
934
|
+
: `Use 'retry' or 'skip' instead.`;
|
|
935
|
+
if (!(0, clean_retry_1.canOfferCleanRetry)(root, state, step, head)) {
|
|
936
|
+
return {
|
|
937
|
+
kind: 'error',
|
|
938
|
+
reason: `Cannot apply action 'retry-clean' to step '${step.id}': ${(0, clean_retry_1.cleanRetryUnavailableReason)(root, state, step, head)} ${fallback}`,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
// Reset under the reservation: the checks above ran on a snapshot, and
|
|
942
|
+
// resetForCleanRetry re-checks against the state read once it is held.
|
|
943
|
+
try {
|
|
944
|
+
await (0, broker_1.resetStepTree)(dir, step, () => (0, clean_retry_1.resetForCleanRetry)(root, dir, step.id), scope);
|
|
945
|
+
}
|
|
946
|
+
catch (e) {
|
|
947
|
+
if (e instanceof broker_1.BrokerStaleRequestError ||
|
|
948
|
+
e instanceof broker_1.BrokerUnavailableError ||
|
|
949
|
+
e instanceof broker_1.TreeBusyError) {
|
|
950
|
+
throw e;
|
|
951
|
+
}
|
|
915
952
|
return {
|
|
916
953
|
kind: 'error',
|
|
917
|
-
reason: `Cannot apply action 'retry-clean' to step '${step.id}': ${
|
|
954
|
+
reason: `Cannot apply action 'retry-clean' to step '${step.id}': the reset to ${step.gitRefBefore} failed: ${e instanceof Error ? e.message : String(e)} ${fallback}`,
|
|
918
955
|
};
|
|
919
956
|
}
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
// whether the reset actually happened. Anything but a verified-clean tree
|
|
923
|
-
// is refused: accepting would drop the generator marker and rerun the
|
|
957
|
+
// Only the tree can say whether the reset left it clean. Anything else is
|
|
958
|
+
// refused: accepting would rearm a step whose next attempt reruns the
|
|
924
959
|
// generator over the previous attempt's output.
|
|
925
960
|
if ((0, git_utils_1.getWorkingTreeStatus)(root) !== 'clean') {
|
|
926
961
|
return {
|
|
927
962
|
kind: 'error',
|
|
928
|
-
reason: `Cannot apply action 'retry-clean' to step '${step.id}': the working tree is not verifiably clean
|
|
963
|
+
reason: `Cannot apply action 'retry-clean' to step '${step.id}': the working tree is not verifiably clean after the reset to ${step.gitRefBefore}. Inspect it with \`git status\`, then re-run it. ${fallback}`,
|
|
929
964
|
};
|
|
930
965
|
}
|
|
931
966
|
}
|
|
@@ -940,7 +975,7 @@ function applyReconcileStepAction(root, state, action) {
|
|
|
940
975
|
if (safety.kind === 'unsafe') {
|
|
941
976
|
return {
|
|
942
977
|
kind: 'error',
|
|
943
|
-
reason: `Cannot apply action 'retry' to step '${step.id}': ${safety.reason} Use 'retry-clean' where offered, or 'skip'.`,
|
|
978
|
+
reason: `Cannot apply action 'retry' to step '${step.id}': ${safety.reason} Use 'retry-clean' where offered${(0, state_machine_1.commitMayBeInHistory)(state, step) ? '' : `, or 'skip'`}.`,
|
|
944
979
|
};
|
|
945
980
|
}
|
|
946
981
|
if (safety.kind === 'warned') {
|
|
@@ -962,46 +997,45 @@ function applyReconcileStepAction(root, state, action) {
|
|
|
962
997
|
return { kind: 'ok', state: applied.state, targetStep: step };
|
|
963
998
|
}
|
|
964
999
|
// Commits the working tree left by a folded prompt outcome or an adopted
|
|
965
|
-
// death
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
|
|
970
|
-
// write refolds on the next reconcile, where the commit attempt sees a clean
|
|
971
|
-
// tree ('no-changes') and the ledger simply misses that landed entry; the
|
|
972
|
-
// changes themselves are never lost. A lost landed entry can also strand the
|
|
973
|
-
// failed entries it had absorbed, which is why completion double-checks the
|
|
974
|
-
// tree before warning about debt.
|
|
975
|
-
async function commitForStep(root, dir, state, step) {
|
|
1000
|
+
// death. The caller persists `entry` with the step transition unless
|
|
1001
|
+
// `recorded` says a session's parent already did; null when nothing to
|
|
1002
|
+
// commit. A crash between the git commit and the state write leaves that
|
|
1003
|
+
// commit in history and out of the ledger; the refold then sees a clean tree.
|
|
1004
|
+
async function commitForStep(root, dir, state, step, scope) {
|
|
976
1005
|
const { name } = (0, state_machine_1.splitMigrationId)(step.migrationId);
|
|
977
1006
|
const absorbedStepIds = (0, state_machine_1.uncoveredFailedStepIds)(state).filter((id) => id !== step.id);
|
|
978
|
-
|
|
1007
|
+
const skipInstall = state.skipInstall === true;
|
|
1008
|
+
let commit;
|
|
979
1009
|
try {
|
|
980
|
-
|
|
1010
|
+
commit = await (0, broker_1.commitStepTree)(dir, step, absorbedStepIds, () => (0, migrate_commits_1.commitMigrationIfRequested)(root, { name }, true, state.commitPrefix, () => (0, util_1.installDepsChangedSinceDispense)(root, dir, step, skipInstall, reconcileCommand(root, state.runId)), (0, state_machine_1.stepsToPendingMigrations)(state, absorbedStepIds)), scope);
|
|
981
1011
|
}
|
|
982
1012
|
catch (e) {
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1013
|
+
// Nothing to record: the step moved on, another process holds the tree, or
|
|
1014
|
+
// the parent never answered. The next reconcile redoes this fold or adopt.
|
|
1015
|
+
if (e instanceof broker_1.BrokerStaleRequestError ||
|
|
1016
|
+
e instanceof broker_1.BrokerUnavailableError ||
|
|
1017
|
+
e instanceof broker_1.TreeBusyError) {
|
|
1018
|
+
throw e;
|
|
1019
|
+
}
|
|
1020
|
+
// The dependency install is the only other thrower: the commit attempt
|
|
1021
|
+
// reports through result.status. The debt cannot stand in for the install
|
|
1022
|
+
// failure, since a later commit absorbing this diff clears the debt while
|
|
1023
|
+
// the dependencies are still missing.
|
|
990
1024
|
(0, util_1.warnCommitFailed)(name, e);
|
|
991
1025
|
return {
|
|
992
1026
|
entry: { kind: 'failed', stepIds: [step.id] },
|
|
993
1027
|
installFailed: true,
|
|
994
1028
|
};
|
|
995
1029
|
}
|
|
996
|
-
if (result.status === 'failed') {
|
|
1030
|
+
if (commit.result.status === 'failed') {
|
|
997
1031
|
(0, util_1.warnCommitFailed)(name);
|
|
998
1032
|
}
|
|
999
1033
|
return {
|
|
1000
|
-
entry: (0, state_machine_1.commitResultToLedgerEntry)(result, step.id, absorbedStepIds),
|
|
1034
|
+
entry: (0, state_machine_1.commitResultToLedgerEntry)(commit.result, step.id, commit.absorbedStepIds),
|
|
1001
1035
|
installFailed: false,
|
|
1036
|
+
recorded: commit.recorded,
|
|
1002
1037
|
};
|
|
1003
1038
|
}
|
|
1004
|
-
// --- dispense ---------------------------------------------------------------
|
|
1005
1039
|
function advanceAndDispense(root, dir, runId) {
|
|
1006
1040
|
// Demote recorded issues no remaining step can claim before anything is
|
|
1007
1041
|
// rendered. Done here, at the single choke point every dispense, retry
|
|
@@ -1024,6 +1058,14 @@ function advanceAndDispense(root, dir, runId) {
|
|
|
1024
1058
|
// above repeats by design, and a rejected --step-action ends the reconcile
|
|
1025
1059
|
// before reaching here, already naming its own fix.
|
|
1026
1060
|
const noProgress = trackNoProgress(dir, step);
|
|
1061
|
+
// Another live process holds the tree: the responses below would offer
|
|
1062
|
+
// actions it refuses or name a pid already gone. A running step's own
|
|
1063
|
+
// operation with a live worker keeps still-running, which offers none.
|
|
1064
|
+
const held = (0, broker_1.liveTreeOperation)(state);
|
|
1065
|
+
if (held && !isOwnOperation(step, held)) {
|
|
1066
|
+
emitHeld(root, runId, step, held, noProgress);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1027
1069
|
switch (step.status) {
|
|
1028
1070
|
case 'pending':
|
|
1029
1071
|
dispenseNextStep(root, dir, runId, state, step, noProgress);
|
|
@@ -1039,7 +1081,7 @@ function advanceAndDispense(root, dir, runId) {
|
|
|
1039
1081
|
emitDied(root, runId, state, step, noProgress);
|
|
1040
1082
|
break;
|
|
1041
1083
|
case 'running':
|
|
1042
|
-
emitStillRunning(root, runId, step, noProgress);
|
|
1084
|
+
emitStillRunning(root, runId, step, held, noProgress);
|
|
1043
1085
|
break;
|
|
1044
1086
|
case 'awaiting-prompt-outcome':
|
|
1045
1087
|
emitAwaitPrompt(root, dir, runId, step, noProgress);
|
|
@@ -1058,6 +1100,12 @@ function advanceAndDispense(root, dir, runId) {
|
|
|
1058
1100
|
}
|
|
1059
1101
|
}
|
|
1060
1102
|
}
|
|
1103
|
+
function isOwnOperation(step, held) {
|
|
1104
|
+
return (step.status === 'running' &&
|
|
1105
|
+
held.stepId === step.id &&
|
|
1106
|
+
step.pid !== undefined &&
|
|
1107
|
+
(0, util_1.isPidAlive)(step.pid));
|
|
1108
|
+
}
|
|
1061
1109
|
function firstActionableStep(state) {
|
|
1062
1110
|
return state.steps.find((s) => !run_state_1.TERMINAL_STEP_STATUSES.has(s.status));
|
|
1063
1111
|
}
|
|
@@ -1107,6 +1155,7 @@ function dispenseNextStep(root, dir, runId, state, step, noProgress) {
|
|
|
1107
1155
|
depsHashAtDispense: (0, util_1.depsHash)(root),
|
|
1108
1156
|
};
|
|
1109
1157
|
let advancedElsewhere = false;
|
|
1158
|
+
let held;
|
|
1110
1159
|
const current = (0, state_lock_1.updateRunState)(dir, (fresh) => {
|
|
1111
1160
|
// A concurrent init or reconcile may have dispensed (or further advanced)
|
|
1112
1161
|
// this step since the caller's read; reclassify against the fresh state
|
|
@@ -1115,6 +1164,12 @@ function dispenseNextStep(root, dir, runId, state, step, noProgress) {
|
|
|
1115
1164
|
advancedElsewhere = true;
|
|
1116
1165
|
return null;
|
|
1117
1166
|
}
|
|
1167
|
+
// A checkpoint in flight would capture the dispensed step's changes;
|
|
1168
|
+
// checked in the write that dispenses, since it can start after any
|
|
1169
|
+
// earlier read.
|
|
1170
|
+
held = (0, broker_1.liveTreeOperation)(fresh);
|
|
1171
|
+
if (held)
|
|
1172
|
+
return null;
|
|
1118
1173
|
const dispensed = applyEventOrThrow(fresh, {
|
|
1119
1174
|
type: 'dispense',
|
|
1120
1175
|
stepId: step.id,
|
|
@@ -1127,6 +1182,10 @@ function dispenseNextStep(root, dir, runId, state, step, noProgress) {
|
|
|
1127
1182
|
advanceAndDispense(root, dir, runId);
|
|
1128
1183
|
return;
|
|
1129
1184
|
}
|
|
1185
|
+
if (held) {
|
|
1186
|
+
emitHeld(root, runId, step, held, noProgress);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1130
1189
|
emitNextStep(root, runId, current, current.steps.find((s) => s.id === step.id), noProgress);
|
|
1131
1190
|
}
|
|
1132
1191
|
function emitNextStep(root, runId, state, step, noProgress) {
|
|
@@ -1147,7 +1206,7 @@ function emitRetryFailed(root, runId, state, step, noProgress) {
|
|
|
1147
1206
|
const summary = step.outcome?.summary ?? step.promptOutcome?.summary;
|
|
1148
1207
|
const head = (0, git_utils_1.getLatestCommitSha)(root);
|
|
1149
1208
|
const tree = dirtyTreeSummary(root);
|
|
1150
|
-
const cleanRetry = canOfferCleanRetry(root, state, step, head);
|
|
1209
|
+
const cleanRetry = (0, clean_retry_1.canOfferCleanRetry)(root, state, step, head);
|
|
1151
1210
|
// A failure recorded before the generator marker can still have written to
|
|
1152
1211
|
// the tree (a direct fs or exec side effect, or a crash mid-flush); a
|
|
1153
1212
|
// marker means only the handed-back half (a prompt or a validation pass)
|
|
@@ -1169,9 +1228,12 @@ function emitRetryFailed(root, runId, state, step, noProgress) {
|
|
|
1169
1228
|
retryOptionLine(retrySafety, reconcileCommand(root, runId, 'retry')),
|
|
1170
1229
|
];
|
|
1171
1230
|
if (cleanRetry) {
|
|
1172
|
-
lines.push(` retry-clean:
|
|
1231
|
+
lines.push(` retry-clean: reset the tree to ${step.gitRefBefore ?? 'the pre-migration ref'} (discarding its uncommitted tracked changes and the untracked files git does not ignore, ${types_1.MIGRATE_RUNS_RELATIVE_DIR} kept) and retry from that clean state by running: ${reconcileCommand(root, runId, 'retry-clean')}`);
|
|
1232
|
+
}
|
|
1233
|
+
// Refused by the state machine: the migration is, or may be, committed.
|
|
1234
|
+
if (!(0, state_machine_1.commitMayBeInHistory)(state, step)) {
|
|
1235
|
+
lines.push(` skip: ${reconcileCommand(root, runId, 'skip')}`);
|
|
1173
1236
|
}
|
|
1174
|
-
lines.push(` skip: ${reconcileCommand(root, runId, 'skip')}`);
|
|
1175
1237
|
if (pending) {
|
|
1176
1238
|
lines.push(UNVERIFIABLE_WRITES_LINE);
|
|
1177
1239
|
}
|
|
@@ -1229,62 +1291,6 @@ function retryOptionLine(safety, command) {
|
|
|
1229
1291
|
}
|
|
1230
1292
|
}
|
|
1231
1293
|
}
|
|
1232
|
-
// A clean retry resets the tree to the step's captured pre-migration ref.
|
|
1233
|
-
// That is only safe when every prior diff is already committed: without
|
|
1234
|
-
// per-migration commits the ref is the run's starting commit (the reset would
|
|
1235
|
-
// wipe all prior steps' uncommitted work); a failed init checkpoint or a
|
|
1236
|
-
// pending step commit means the ref predates diffs the reset would also
|
|
1237
|
-
// destroy; without a captured ref there is nothing to reset to; edits already
|
|
1238
|
-
// in the tree when this step was dispensed (the user's own, or an earlier
|
|
1239
|
-
// step's the checkpoint never saw) are not represented by the ref either; and
|
|
1240
|
-
// HEAD anywhere other than the ref means something was committed since the
|
|
1241
|
-
// step was dispensed that the reset would discard, whether that is this step's
|
|
1242
|
-
// own commit (recorded, or made in the window before the worker died writing
|
|
1243
|
-
// its ledger entry) or one the user made alongside the run.
|
|
1244
|
-
// Cleanliness and position both have to say so explicitly: a failed tree probe
|
|
1245
|
-
// records dirty, a run created before that field existed carries nothing to
|
|
1246
|
-
// check, and an unreadable HEAD is no ref at all, so none of the three can be
|
|
1247
|
-
// read as a restore point that exists.
|
|
1248
|
-
// The debt check is run-wide: a clean retry resets and cleans the whole tree,
|
|
1249
|
-
// which would discard every other failed step's uncommitted work as well.
|
|
1250
|
-
function canOfferCleanRetry(root, state, step, head) {
|
|
1251
|
-
return (state.createCommits &&
|
|
1252
|
-
!state.checkpointFailed &&
|
|
1253
|
-
!(0, state_machine_1.hasPendingCommitDebt)(state) &&
|
|
1254
|
-
!!step.gitRefBefore &&
|
|
1255
|
-
head === step.gitRefBefore &&
|
|
1256
|
-
step.treeCleanAtDispense === true &&
|
|
1257
|
-
!endangeredLandedEntry(root, state, step));
|
|
1258
|
-
}
|
|
1259
|
-
// The last landed ledger entry covering the step whose commit a reset to the
|
|
1260
|
-
// step's gitRefBefore would discard. Entries from earlier attempts predate the
|
|
1261
|
-
// ref re-captured at re-dispense and survive the reset; only a commit that is
|
|
1262
|
-
// not an ancestor of the ref (or cannot be verified as one) is endangered.
|
|
1263
|
-
function endangeredLandedEntry(root, state, step) {
|
|
1264
|
-
let endangered = null;
|
|
1265
|
-
for (const entry of (0, state_machine_1.coveringLandedEntries)(state, step.id)) {
|
|
1266
|
-
if (!entry.sha ||
|
|
1267
|
-
!step.gitRefBefore ||
|
|
1268
|
-
!(0, git_utils_1.isAncestorCommit)(entry.sha, step.gitRefBefore, root)) {
|
|
1269
|
-
endangered = entry;
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
return endangered;
|
|
1273
|
-
}
|
|
1274
|
-
// Explains why retry-clean is withheld for a failed or died step; feeds the
|
|
1275
|
-
// death dispense and a rejected --step-action=retry-clean.
|
|
1276
|
-
function cleanRetryUnavailableReason(root, state, step, head) {
|
|
1277
|
-
const endangered = endangeredLandedEntry(root, state, step);
|
|
1278
|
-
if (endangered) {
|
|
1279
|
-
return endangered.sha
|
|
1280
|
-
? `this migration's changes already landed in commit ${endangered.sha}, which a reset would discard.`
|
|
1281
|
-
: `this migration's changes already landed in a commit, which a reset would discard.`;
|
|
1282
|
-
}
|
|
1283
|
-
if (step.gitRefBefore && head !== step.gitRefBefore) {
|
|
1284
|
-
return `HEAD is at ${head ?? '(unreadable)'} rather than the ${step.gitRefBefore} this migration started from, so a reset would discard what was committed in between.`;
|
|
1285
|
-
}
|
|
1286
|
-
return `resetting the tree could discard uncommitted work that no restore point accounts for.`;
|
|
1287
|
-
}
|
|
1288
1294
|
function assessPreMarkerRetry(root, step) {
|
|
1289
1295
|
const repo = (0, git_utils_1.getGitRepositoryStatus)(root);
|
|
1290
1296
|
if (repo === 'not-git') {
|
|
@@ -1319,7 +1325,7 @@ function emitDied(root, runId, state, step, noProgress) {
|
|
|
1319
1325
|
const ref = step.gitRefBefore;
|
|
1320
1326
|
const head = (0, git_utils_1.getLatestCommitSha)(root);
|
|
1321
1327
|
const tree = dirtyTreeSummary(root);
|
|
1322
|
-
const cleanRetry = canOfferCleanRetry(root, state, step, head);
|
|
1328
|
+
const cleanRetry = (0, clean_retry_1.canOfferCleanRetry)(root, state, step, head);
|
|
1323
1329
|
const resume = !generatorPending(step);
|
|
1324
1330
|
const capReached = rearmCapReached(step);
|
|
1325
1331
|
const lines = [
|
|
@@ -1335,15 +1341,16 @@ function emitDied(root, runId, state, step, noProgress) {
|
|
|
1335
1341
|
options.push(` retry: keep everything this migration already produced (its commit, if any, and the current tree) and run only the part that did not complete, then run: ${reconcileCommand(root, runId, 'retry')}`);
|
|
1336
1342
|
}
|
|
1337
1343
|
if (cleanRetry) {
|
|
1338
|
-
options.push(
|
|
1339
|
-
// Two commands rather than one `&&` chain: the agent runs these in its
|
|
1340
|
-
// own shell, and not every shell joins statements that way.
|
|
1341
|
-
` retry-clean: restore the tree to ${ref ?? 'the pre-migration ref'} first (e.g. \`git reset --hard ${ref ?? '<ref>'}\` then \`git clean -fd -e ${types_1.MIGRATE_RUNS_RELATIVE_DIR}\`, keeping the run state out of the clean), then retry from that clean state by running: ${reconcileCommand(root, runId, 'retry-clean')}`);
|
|
1344
|
+
options.push(` retry-clean: reset the tree to ${ref ?? 'the pre-migration ref'} (discarding its uncommitted tracked changes and the untracked files git does not ignore, ${types_1.MIGRATE_RUNS_RELATIVE_DIR} kept) and retry from that clean state by running: ${reconcileCommand(root, runId, 'retry-clean')}`);
|
|
1342
1345
|
}
|
|
1343
1346
|
else {
|
|
1344
|
-
lines.push(`A clean retry is unavailable: ${cleanRetryUnavailableReason(root, state, step, head)}`);
|
|
1347
|
+
lines.push(`A clean retry is unavailable: ${(0, clean_retry_1.cleanRetryUnavailableReason)(root, state, step, head)}`);
|
|
1348
|
+
}
|
|
1349
|
+
options.push(` adopt: keep the current working-tree state as this migration's result, then run: ${reconcileCommand(root, runId, 'adopt')}`);
|
|
1350
|
+
// Refused by the state machine: the migration is, or may be, committed.
|
|
1351
|
+
if (!(0, state_machine_1.commitMayBeInHistory)(state, step)) {
|
|
1352
|
+
options.push(` skip: leave the tree as it stands and move on without this migration, then run: ${reconcileCommand(root, runId, 'skip')}`);
|
|
1345
1353
|
}
|
|
1346
|
-
options.push(` adopt: keep the current working-tree state as this migration's result, then run: ${reconcileCommand(root, runId, 'adopt')}`, ` skip: leave the tree as it stands and move on without this migration, then run: ${reconcileCommand(root, runId, 'skip')}`);
|
|
1347
1354
|
lines.push(`Choose exactly one:`);
|
|
1348
1355
|
lines.push(...options);
|
|
1349
1356
|
if (!resume) {
|
|
@@ -1363,14 +1370,30 @@ function emitDied(root, runId, state, step, noProgress) {
|
|
|
1363
1370
|
instructionLines: lines,
|
|
1364
1371
|
}, noProgress);
|
|
1365
1372
|
}
|
|
1366
|
-
function
|
|
1373
|
+
function emitHeld(root, runId, step, held, noProgress) {
|
|
1374
|
+
emit(root, runId, step, 'held', {
|
|
1375
|
+
next: reconcileCommand(root, runId),
|
|
1376
|
+
instructionLines: [(0, broker_1.treeBusyMessage)(held)],
|
|
1377
|
+
}, noProgress);
|
|
1378
|
+
}
|
|
1379
|
+
function emitStillRunning(root, runId, step, held, noProgress) {
|
|
1367
1380
|
const migrationId = step.migrationId;
|
|
1368
1381
|
const ageMs = step.startedAt ? Date.now() - Date.parse(step.startedAt) : 0;
|
|
1369
1382
|
const lines = [
|
|
1370
1383
|
`The worker for ${migrationId} (pid ${step.pid}) is still running. Wait for it to finish, then run the "next" command.`,
|
|
1371
1384
|
];
|
|
1385
|
+
// A holder pid other than the worker's is the session's parent process.
|
|
1386
|
+
const parentOperation = held !== undefined && held.pid !== step.pid
|
|
1387
|
+
? (0, broker_1.treeOperationLabel)(held)
|
|
1388
|
+
: undefined;
|
|
1389
|
+
if (parentOperation) {
|
|
1390
|
+
lines.push(`The nx process ${held.pid} is running ${parentOperation} for it; killing the worker does not stop that, and the step can be classified as died only once it finishes.`);
|
|
1391
|
+
}
|
|
1372
1392
|
if (ageMs >= HANG_THRESHOLD_MS) {
|
|
1373
|
-
|
|
1393
|
+
const hung = `It has been running for ${Math.floor(ageMs / 60000)} minutes and may be hung.`;
|
|
1394
|
+
lines.push(parentOperation
|
|
1395
|
+
? `${hung} Report this to the user: they can quit this session, then press Ctrl+C in the terminal to end ${parentOperation}, and resume the run afterwards.`
|
|
1396
|
+
: `${hung} Verify pid ${step.pid}; either keep waiting, or kill it so the next reconcile can classify it as died.`);
|
|
1374
1397
|
}
|
|
1375
1398
|
emit(root, runId, step, 'still-running', {
|
|
1376
1399
|
next: reconcileCommand(root, runId),
|
|
@@ -1564,16 +1587,44 @@ function emitError(root, runId, reason) {
|
|
|
1564
1587
|
});
|
|
1565
1588
|
(0, migrate_analytics_1.reportMigrateOrchestratorDispense)({ action: 'error', attempt: 0 });
|
|
1566
1589
|
}
|
|
1590
|
+
/**
|
|
1591
|
+
* What a completed run leaves behind for whoever reads its summary, one line
|
|
1592
|
+
* group per warning: commit debt, uninstalled dependency changes, unresolved
|
|
1593
|
+
* issues. Empty when nothing is left.
|
|
1594
|
+
*/
|
|
1595
|
+
function completionWarnings(root, runId, state) {
|
|
1596
|
+
// The crash-refold window can strand a failed ledger entry whose diff was in
|
|
1597
|
+
// fact absorbed; suppress the warning only on a verified-clean tree. A dirty
|
|
1598
|
+
// tree can still be unrelated edits, so the warning only claims the changes
|
|
1599
|
+
// "may remain".
|
|
1600
|
+
const commitDebt = (0, state_machine_1.hasPendingCommitDebt)(state) && (0, git_utils_1.getWorkingTreeStatus)(root) !== 'clean';
|
|
1601
|
+
const uninstalled = state.steps.filter((s) => s.installFailed);
|
|
1602
|
+
const issueLines = (0, issues_1.renderUnresolvedIssueLines)(state, runId);
|
|
1603
|
+
return [
|
|
1604
|
+
...(commitDebt
|
|
1605
|
+
? [
|
|
1606
|
+
[
|
|
1607
|
+
'Some migration changes could not be committed and may remain in the working tree; review and commit them manually.',
|
|
1608
|
+
],
|
|
1609
|
+
]
|
|
1610
|
+
: []),
|
|
1611
|
+
...(uninstalled.length > 0
|
|
1612
|
+
? [
|
|
1613
|
+
[
|
|
1614
|
+
`The dependency changes made by ${uninstalled
|
|
1615
|
+
.map((s) => s.migrationId)
|
|
1616
|
+
.join(', ')} were not installed; run \`${(0, util_1.pmInstallCommand)(root)}\` before using the workspace.`,
|
|
1617
|
+
],
|
|
1618
|
+
]
|
|
1619
|
+
: []),
|
|
1620
|
+
...(issueLines.length > 0 ? [issueLines] : []),
|
|
1621
|
+
];
|
|
1622
|
+
}
|
|
1567
1623
|
function completeRun(root, dir, runId, state) {
|
|
1568
1624
|
let current = state;
|
|
1569
1625
|
const completed = current.steps.filter((s) => s.status === 'succeeded').length;
|
|
1570
1626
|
const skipped = current.steps.filter((s) => s.status === 'skipped').length;
|
|
1571
1627
|
const dispenseCount = current.steps.reduce((n, s) => n + s.dispenseCount, 0);
|
|
1572
|
-
// The crash-refold window can strand a failed ledger entry whose diff was in
|
|
1573
|
-
// fact absorbed; suppress the warning only on a verified-clean tree. A dirty
|
|
1574
|
-
// tree can still be unrelated edits, so the warning only claims the changes
|
|
1575
|
-
// "may remain".
|
|
1576
|
-
const commitDebt = (0, state_machine_1.hasPendingCommitDebt)(current) && (0, git_utils_1.getWorkingTreeStatus)(root) !== 'clean';
|
|
1577
1628
|
// Persist the terminal status and claim the watermark in one fresh-state
|
|
1578
1629
|
// write before emitting: a crash between the write and the output can't
|
|
1579
1630
|
// double-count the completion, and of two concurrent reconciles exactly one
|
|
@@ -1599,30 +1650,15 @@ function completeRun(root, dir, runId, state) {
|
|
|
1599
1650
|
dispenseCount,
|
|
1600
1651
|
});
|
|
1601
1652
|
}
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
(0, agent_output_1.warnToAgent)({ title:
|
|
1605
|
-
}
|
|
1606
|
-
const uninstalled = current.steps.filter((s) => s.installFailed);
|
|
1607
|
-
const installLine = uninstalled.length > 0
|
|
1608
|
-
? `The dependency changes made by ${uninstalled
|
|
1609
|
-
.map((s) => s.migrationId)
|
|
1610
|
-
.join(', ')} were not installed; run \`${(0, util_1.pmInstallCommand)(root)}\` before using the workspace.`
|
|
1611
|
-
: null;
|
|
1612
|
-
if (installLine) {
|
|
1613
|
-
(0, agent_output_1.warnToAgent)({ title: installLine });
|
|
1614
|
-
}
|
|
1615
|
-
const issueLines = (0, issues_1.renderUnresolvedIssueLines)(current, runId);
|
|
1616
|
-
if (issueLines.length > 0) {
|
|
1617
|
-
(0, agent_output_1.warnToAgent)({ title: issueLines[0], bodyLines: issueLines.slice(1) });
|
|
1653
|
+
const warnings = completionWarnings(root, runId, current);
|
|
1654
|
+
for (const lines of warnings) {
|
|
1655
|
+
(0, agent_output_1.warnToAgent)({ title: lines[0], bodyLines: lines.slice(1) });
|
|
1618
1656
|
}
|
|
1619
1657
|
const instructionLines = [
|
|
1620
1658
|
`Migrate run ${runId} is complete.`,
|
|
1621
1659
|
` applied: ${completed}`,
|
|
1622
1660
|
` skipped: ${skipped}`,
|
|
1623
|
-
...(
|
|
1624
|
-
...(installLine ? [installLine] : []),
|
|
1625
|
-
...issueLines,
|
|
1661
|
+
...warnings.flat(),
|
|
1626
1662
|
];
|
|
1627
1663
|
(0, agent_output_1.logToAgent)({ title: 'nx migrate: complete', bodyLines: instructionLines });
|
|
1628
1664
|
(0, agent_output_1.emitStepBlock)(runId, '-', 'complete', {
|
|
@@ -1687,10 +1723,6 @@ function reconcileCommand(root, runId, action) {
|
|
|
1687
1723
|
const base = `${(0, util_1.pmExecPrefix)(root)} nx migrate --run-id=${runId}`;
|
|
1688
1724
|
return action ? `${base} --step-action=${action}` : base;
|
|
1689
1725
|
}
|
|
1690
|
-
// --- helpers ----------------------------------------------------------------
|
|
1691
|
-
function appendCommit(state, entry) {
|
|
1692
|
-
return { ...state, commits: [...state.commits, entry] };
|
|
1693
|
-
}
|
|
1694
1726
|
// Records what the workspace looked like as this attempt starts. The git ref
|
|
1695
1727
|
// and the tree state are re-captured per dispense, since a retry restarts from
|
|
1696
1728
|
// wherever the tree is now. The dependency baseline is not: it tracks the last
|