agentxchain 2.141.1 → 2.142.0
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/package.json
CHANGED
|
@@ -36,8 +36,10 @@ import {
|
|
|
36
36
|
compareDeclaredVsObserved,
|
|
37
37
|
deriveAcceptedRef,
|
|
38
38
|
checkCleanBaseline,
|
|
39
|
+
detectDirtyFilesOutsideAllowed,
|
|
39
40
|
isOperationalPath,
|
|
40
41
|
isBaselineExemptPath,
|
|
42
|
+
normalizeCheckpointableFiles,
|
|
41
43
|
} from './repo-observer.js';
|
|
42
44
|
import { getMaxConcurrentTurns } from './normalized-config.js';
|
|
43
45
|
import { getTurnStagingResultPath, getTurnStagingDir, getDispatchTurnDir, getReviewArtifactPath } from './turn-paths.js';
|
|
@@ -65,6 +67,8 @@ import {
|
|
|
65
67
|
} from './repo-decisions.js';
|
|
66
68
|
import {
|
|
67
69
|
replayVerificationMachineEvidence,
|
|
70
|
+
normalizeVerificationProducedFiles,
|
|
71
|
+
cleanupIgnoredVerificationFiles,
|
|
68
72
|
summarizeVerificationReplay,
|
|
69
73
|
} from './verification-replay.js';
|
|
70
74
|
import { executeGateActions } from './gate-actions.js';
|
|
@@ -3205,7 +3209,16 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3205
3209
|
};
|
|
3206
3210
|
}
|
|
3207
3211
|
|
|
3208
|
-
const
|
|
3212
|
+
const rawTurnResult = validation.turnResult;
|
|
3213
|
+
const verificationProducedFiles = normalizeVerificationProducedFiles(rawTurnResult.verification);
|
|
3214
|
+
const effectiveFilesChanged = normalizeCheckpointableFiles([
|
|
3215
|
+
...(rawTurnResult.files_changed || []),
|
|
3216
|
+
...verificationProducedFiles.artifact_files,
|
|
3217
|
+
]);
|
|
3218
|
+
const turnResult = {
|
|
3219
|
+
...rawTurnResult,
|
|
3220
|
+
files_changed: effectiveFilesChanged,
|
|
3221
|
+
};
|
|
3209
3222
|
|
|
3210
3223
|
// Validate cross-run decision overrides against repo-decisions.jsonl
|
|
3211
3224
|
if (turnResult.decisions && turnResult.decisions.length > 0) {
|
|
@@ -3231,6 +3244,35 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3231
3244
|
const stagingFile = join(root, resolvedStagingPath);
|
|
3232
3245
|
const now = new Date().toISOString();
|
|
3233
3246
|
const baseline = currentTurn.baseline || null;
|
|
3247
|
+
const ignoredVerificationCleanup = cleanupIgnoredVerificationFiles({
|
|
3248
|
+
root,
|
|
3249
|
+
baseline,
|
|
3250
|
+
ignoredFiles: verificationProducedFiles.ignored_files,
|
|
3251
|
+
});
|
|
3252
|
+
if (ignoredVerificationCleanup.ok === false) {
|
|
3253
|
+
const cleanupError = ignoredVerificationCleanup.cleanup_error
|
|
3254
|
+
|| 'Ignored verification-produced files could not be restored safely.';
|
|
3255
|
+
transitionToFailedAcceptance(root, state, currentTurn, cleanupError, {
|
|
3256
|
+
error_code: 'verification_produced_files_cleanup',
|
|
3257
|
+
stage: 'artifact_observation',
|
|
3258
|
+
extra: {
|
|
3259
|
+
ignored_files: verificationProducedFiles.ignored_files,
|
|
3260
|
+
restored_files: ignoredVerificationCleanup.restored_files || [],
|
|
3261
|
+
},
|
|
3262
|
+
});
|
|
3263
|
+
return {
|
|
3264
|
+
ok: false,
|
|
3265
|
+
error: cleanupError,
|
|
3266
|
+
validation: {
|
|
3267
|
+
...validation,
|
|
3268
|
+
ok: false,
|
|
3269
|
+
stage: 'artifact_observation',
|
|
3270
|
+
error_class: 'artifact_error',
|
|
3271
|
+
errors: [cleanupError],
|
|
3272
|
+
warnings: validation.warnings,
|
|
3273
|
+
},
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
3234
3276
|
const rawObservation = observeChanges(root, baseline);
|
|
3235
3277
|
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
3236
3278
|
const pendingConcurrentSiblingDeclarations = collectPendingConcurrentSiblingDeclarations(
|
|
@@ -3268,9 +3310,76 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3268
3310
|
const concurrentIds = new Set(
|
|
3269
3311
|
Array.isArray(currentTurn.concurrent_with) ? currentTurn.concurrent_with : [],
|
|
3270
3312
|
);
|
|
3313
|
+
const concurrentAllowedDirtyFiles = [];
|
|
3314
|
+
for (const declaration of pendingConcurrentSiblingDeclarations) {
|
|
3315
|
+
concurrentIds.add(declaration?.turn_id);
|
|
3316
|
+
for (const filePath of Array.isArray(declaration?.files_changed) ? declaration.files_changed : []) {
|
|
3317
|
+
concurrentAllowedDirtyFiles.push(filePath);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
for (const historyEntry of historyEntries) {
|
|
3321
|
+
const isConcurrentSibling = concurrentIds.has(historyEntry?.turn_id)
|
|
3322
|
+
|| (Array.isArray(historyEntry?.concurrent_with) && historyEntry.concurrent_with.includes(currentTurn.turn_id));
|
|
3323
|
+
if (!isConcurrentSibling) {
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
const siblingFiles = Array.isArray(historyEntry?.observed_artifact?.files_changed)
|
|
3327
|
+
? historyEntry.observed_artifact.files_changed
|
|
3328
|
+
: historyEntry?.files_changed;
|
|
3329
|
+
for (const filePath of Array.isArray(siblingFiles) ? siblingFiles : []) {
|
|
3330
|
+
concurrentAllowedDirtyFiles.push(filePath);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3271
3333
|
const acceptedTurnIds = new Set(historyEntries.map(h => h.turn_id));
|
|
3272
3334
|
const hasUnacceptedConcurrentSiblings = [...concurrentIds].some(id => !acceptedTurnIds.has(id));
|
|
3273
3335
|
|
|
3336
|
+
// Files from accepted-but-uncheckpointed prior turns are expected to be
|
|
3337
|
+
// dirty in the working tree until `checkpoint-turn` commits them. These
|
|
3338
|
+
// must not block the current turn's acceptance — they are known accepted
|
|
3339
|
+
// mutations, not undeclared agent writes.
|
|
3340
|
+
const uncheckpointedPriorFiles = [];
|
|
3341
|
+
for (const historyEntry of historyEntries) {
|
|
3342
|
+
if (historyEntry.checkpoint_sha) continue; // already committed
|
|
3343
|
+
const priorFiles = Array.isArray(historyEntry?.files_changed)
|
|
3344
|
+
? historyEntry.files_changed
|
|
3345
|
+
: [];
|
|
3346
|
+
for (const filePath of priorFiles) {
|
|
3347
|
+
uncheckpointedPriorFiles.push(filePath);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
const dirtyParity = detectDirtyFilesOutsideAllowed(
|
|
3352
|
+
root,
|
|
3353
|
+
writeAuthority,
|
|
3354
|
+
[
|
|
3355
|
+
...(turnResult.files_changed || []),
|
|
3356
|
+
...concurrentAllowedDirtyFiles,
|
|
3357
|
+
...uncheckpointedPriorFiles,
|
|
3358
|
+
],
|
|
3359
|
+
);
|
|
3360
|
+
if (!dirtyParity.clean) {
|
|
3361
|
+
transitionToFailedAcceptance(root, state, currentTurn, dirtyParity.reason, {
|
|
3362
|
+
error_code: 'artifact_dirty_tree_mismatch',
|
|
3363
|
+
stage: 'artifact_observation',
|
|
3364
|
+
extra: {
|
|
3365
|
+
unexpected_dirty_files: dirtyParity.unexpected_dirty_files,
|
|
3366
|
+
dirty_files: dirtyParity.dirty_files,
|
|
3367
|
+
},
|
|
3368
|
+
});
|
|
3369
|
+
return {
|
|
3370
|
+
ok: false,
|
|
3371
|
+
error: dirtyParity.reason,
|
|
3372
|
+
validation: {
|
|
3373
|
+
...validation,
|
|
3374
|
+
ok: false,
|
|
3375
|
+
stage: 'artifact_observation',
|
|
3376
|
+
error_class: 'artifact_error',
|
|
3377
|
+
errors: [dirtyParity.reason],
|
|
3378
|
+
warnings: validation.warnings,
|
|
3379
|
+
},
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3274
3383
|
const diffComparison = compareDeclaredVsObserved(
|
|
3275
3384
|
turnResult.files_changed || [],
|
|
3276
3385
|
observation.files_changed,
|
|
@@ -3433,6 +3542,41 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3433
3542
|
const observedArtifact = buildObservedArtifact(observation, baseline);
|
|
3434
3543
|
const normalizedVerification = normalizeVerification(turnResult.verification, runtimeType);
|
|
3435
3544
|
const artifactType = turnResult.artifact?.type || 'review';
|
|
3545
|
+
|
|
3546
|
+
// BUG-46 fix requirement #6: workspace artifact with empty files_changed is a
|
|
3547
|
+
// protocol violation for authoritative completed turns. A workspace artifact
|
|
3548
|
+
// declares "I mutated repo files" — if files_changed is empty, the declaration
|
|
3549
|
+
// is incoherent and must be rejected before replay or history persistence.
|
|
3550
|
+
if (artifactType === 'workspace'
|
|
3551
|
+
&& writeAuthority === 'authoritative'
|
|
3552
|
+
&& turnResult.status === 'completed'
|
|
3553
|
+
&& (turnResult.files_changed || []).length === 0
|
|
3554
|
+
&& (observation.files_changed || []).length === 0) {
|
|
3555
|
+
const emptyWsError = 'Turn declared artifact.type: "workspace" but files_changed is empty. '
|
|
3556
|
+
+ 'Either declare the files modified, or set artifact.type: "review" if no repo mutations were intended.';
|
|
3557
|
+
transitionToFailedAcceptance(root, state, currentTurn, emptyWsError, {
|
|
3558
|
+
error_code: 'empty_workspace_artifact',
|
|
3559
|
+
stage: 'artifact_validation',
|
|
3560
|
+
extra: {
|
|
3561
|
+
artifact_type: artifactType,
|
|
3562
|
+
write_authority: writeAuthority,
|
|
3563
|
+
turn_status: turnResult.status,
|
|
3564
|
+
},
|
|
3565
|
+
});
|
|
3566
|
+
return {
|
|
3567
|
+
ok: false,
|
|
3568
|
+
error: emptyWsError,
|
|
3569
|
+
validation: {
|
|
3570
|
+
...validation,
|
|
3571
|
+
ok: false,
|
|
3572
|
+
stage: 'artifact_validation',
|
|
3573
|
+
error_class: 'artifact_error',
|
|
3574
|
+
errors: [emptyWsError],
|
|
3575
|
+
warnings: validation.warnings,
|
|
3576
|
+
},
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3436
3580
|
const derivedRef = deriveAcceptedRef(observation, artifactType, state.accepted_integration_ref);
|
|
3437
3581
|
const verificationReplay = (config.policies || []).some((policy) => policy?.rule === 'require_reproducible_verification')
|
|
3438
3582
|
? replayVerificationMachineEvidence({ root, verification: turnResult.verification })
|
|
@@ -3740,7 +3884,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3740
3884
|
summary: turnResult.summary,
|
|
3741
3885
|
decisions: turnResult.decisions || [],
|
|
3742
3886
|
objections: turnResult.objections || [],
|
|
3743
|
-
files_changed: turnResult.files_changed
|
|
3887
|
+
files_changed: normalizeCheckpointableFiles(turnResult.files_changed),
|
|
3744
3888
|
artifacts_created: turnResult.artifacts_created || [],
|
|
3745
3889
|
verification: turnResult.verification || {},
|
|
3746
3890
|
normalized_verification: normalizedVerification,
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -79,6 +79,16 @@ export function isBaselineExemptPath(filePath) {
|
|
|
79
79
|
|| BASELINE_EXEMPT_PATH_PREFIXES.some(prefix => filePath.startsWith(prefix));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export function normalizeCheckpointableFiles(filesChanged) {
|
|
83
|
+
return [...new Set(
|
|
84
|
+
(Array.isArray(filesChanged) ? filesChanged : [])
|
|
85
|
+
.filter((value) => typeof value === 'string')
|
|
86
|
+
.map((value) => value.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.filter((value) => !isOperationalPath(value)),
|
|
89
|
+
)];
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
// ── Baseline Capture ────────────────────────────────────────────────────────
|
|
83
93
|
|
|
84
94
|
/**
|
|
@@ -631,6 +641,43 @@ export function checkCleanBaseline(root, writeAuthority) {
|
|
|
631
641
|
};
|
|
632
642
|
}
|
|
633
643
|
|
|
644
|
+
export function detectDirtyFilesOutsideAllowed(root, writeAuthority, allowedFiles = []) {
|
|
645
|
+
const cleanCheck = checkCleanBaseline(root, writeAuthority);
|
|
646
|
+
if (cleanCheck.clean) {
|
|
647
|
+
return {
|
|
648
|
+
clean: true,
|
|
649
|
+
dirty_files: [],
|
|
650
|
+
unexpected_dirty_files: [],
|
|
651
|
+
reason: null,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const allowedSet = new Set(
|
|
656
|
+
(Array.isArray(allowedFiles) ? allowedFiles : [])
|
|
657
|
+
.filter((value) => typeof value === 'string')
|
|
658
|
+
.map((value) => value.trim())
|
|
659
|
+
.filter(Boolean),
|
|
660
|
+
);
|
|
661
|
+
const dirtyFiles = Array.isArray(cleanCheck.dirty_files) ? cleanCheck.dirty_files : [];
|
|
662
|
+
const unexpectedDirtyFiles = dirtyFiles.filter((filePath) => !allowedSet.has(filePath));
|
|
663
|
+
|
|
664
|
+
if (unexpectedDirtyFiles.length === 0) {
|
|
665
|
+
return {
|
|
666
|
+
clean: true,
|
|
667
|
+
dirty_files: dirtyFiles,
|
|
668
|
+
unexpected_dirty_files: [],
|
|
669
|
+
reason: null,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
clean: false,
|
|
675
|
+
dirty_files: dirtyFiles,
|
|
676
|
+
unexpected_dirty_files: unexpectedDirtyFiles,
|
|
677
|
+
reason: `Working tree has uncommitted changes in actor-owned files outside the accepted turn contract: ${unexpectedDirtyFiles.slice(0, 5).join(', ')}${unexpectedDirtyFiles.length > 5 ? '...' : ''}. Resume would block on the same files. Declare them in files_changed, classify verification outputs under verification.produced_files, or clean them before acceptance.`,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
634
681
|
// ── Git Primitives ──────────────────────────────────────────────────────────
|
|
635
682
|
|
|
636
683
|
function isGitRepo(root) {
|
|
@@ -165,6 +165,20 @@
|
|
|
165
165
|
"exit_code": { "type": "integer" }
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
},
|
|
169
|
+
"produced_files": {
|
|
170
|
+
"type": "array",
|
|
171
|
+
"items": {
|
|
172
|
+
"type": "object",
|
|
173
|
+
"required": ["path"],
|
|
174
|
+
"additionalProperties": false,
|
|
175
|
+
"properties": {
|
|
176
|
+
"path": { "type": "string" },
|
|
177
|
+
"disposition": {
|
|
178
|
+
"enum": ["artifact", "ignore"]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
},
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { resolveAcceptedTurnHistoryReference } from './accepted-turn-history.js';
|
|
5
5
|
import { emitRunEvent } from './run-events.js';
|
|
6
6
|
import { safeWriteJson } from './safe-write.js';
|
|
7
|
+
import { normalizeCheckpointableFiles } from './repo-observer.js';
|
|
7
8
|
|
|
8
9
|
const STATE_PATH = '.agentxchain/state.json';
|
|
9
10
|
const HISTORY_PATH = '.agentxchain/history.jsonl';
|
|
@@ -52,25 +53,8 @@ function isGitRepo(root) {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
// BUG-43: Staging and dispatch dirs are ephemeral — cleaned up after acceptance.
|
|
56
|
-
// They must never appear in checkpoint git-add paths.
|
|
57
|
-
const EPHEMERAL_PATH_PREFIXES = [
|
|
58
|
-
'.agentxchain/staging/',
|
|
59
|
-
'.agentxchain/dispatch/',
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
function isEphemeralPath(filePath) {
|
|
63
|
-
return EPHEMERAL_PATH_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
56
|
function normalizeFilesChanged(filesChanged) {
|
|
67
|
-
return
|
|
68
|
-
(Array.isArray(filesChanged) ? filesChanged : [])
|
|
69
|
-
.filter((value) => typeof value === 'string')
|
|
70
|
-
.map((value) => value.trim())
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
.filter((value) => !isEphemeralPath(value)),
|
|
73
|
-
)];
|
|
57
|
+
return normalizeCheckpointableFiles(filesChanged);
|
|
74
58
|
}
|
|
75
59
|
|
|
76
60
|
function extractGitError(err) {
|
|
@@ -624,6 +624,40 @@ function validateVerification(tr) {
|
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
+
if (Array.isArray(v.produced_files)) {
|
|
628
|
+
const seenProducedPaths = new Map();
|
|
629
|
+
const declaredFiles = new Set(Array.isArray(tr.files_changed) ? tr.files_changed : []);
|
|
630
|
+
for (let i = 0; i < v.produced_files.length; i++) {
|
|
631
|
+
const entry = v.produced_files[i];
|
|
632
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
633
|
+
errors.push(`verification.produced_files[${i}] must be an object.`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const rawPath = typeof entry.path === 'string' ? entry.path : '';
|
|
638
|
+
const path = rawPath.trim();
|
|
639
|
+
if (!path) {
|
|
640
|
+
errors.push(`verification.produced_files[${i}].path must be a non-empty string.`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const disposition = entry.disposition == null ? 'artifact' : entry.disposition;
|
|
644
|
+
if (!['artifact', 'ignore'].includes(disposition)) {
|
|
645
|
+
errors.push(`verification.produced_files[${i}].disposition must be "artifact" or "ignore".`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (path) {
|
|
649
|
+
if (seenProducedPaths.has(path)) {
|
|
650
|
+
errors.push(`verification.produced_files[${i}].path duplicates "${path}" from verification.produced_files[${seenProducedPaths.get(path)}].`);
|
|
651
|
+
} else {
|
|
652
|
+
seenProducedPaths.set(path, i);
|
|
653
|
+
}
|
|
654
|
+
if (disposition === 'ignore' && declaredFiles.has(path)) {
|
|
655
|
+
errors.push(`verification.produced_files[${i}] marks "${path}" as ignore, but it is also listed in files_changed.`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
627
661
|
return { errors, warnings };
|
|
628
662
|
}
|
|
629
663
|
|
|
@@ -81,6 +81,93 @@ export function summarizeVerificationReplay(payload) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export function normalizeVerificationProducedFiles(verification) {
|
|
85
|
+
const artifactFiles = new Set();
|
|
86
|
+
const ignoredFiles = new Set();
|
|
87
|
+
const producedFiles = Array.isArray(verification?.produced_files)
|
|
88
|
+
? verification.produced_files
|
|
89
|
+
: [];
|
|
90
|
+
|
|
91
|
+
for (const entry of producedFiles) {
|
|
92
|
+
if (!entry || typeof entry !== 'object') {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const path = typeof entry.path === 'string' ? entry.path.trim() : '';
|
|
96
|
+
if (!path || isOperationalPath(path)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (entry.disposition === 'ignore') {
|
|
100
|
+
ignoredFiles.add(path);
|
|
101
|
+
} else {
|
|
102
|
+
artifactFiles.add(path);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
artifact_files: [...artifactFiles].sort(),
|
|
108
|
+
ignored_files: [...ignoredFiles].sort(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function cleanupIgnoredVerificationFiles({ root, baseline, ignoredFiles }) {
|
|
113
|
+
const targetFiles = [...new Set(Array.isArray(ignoredFiles) ? ignoredFiles : [])]
|
|
114
|
+
.filter((filePath) => typeof filePath === 'string' && filePath && !isOperationalPath(filePath))
|
|
115
|
+
.sort();
|
|
116
|
+
|
|
117
|
+
if (targetFiles.length === 0 || !isGitRepo(root)) {
|
|
118
|
+
return { ok: true, restored_files: [], cleanup_error: null };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const restoredFiles = [];
|
|
122
|
+
const baselineSnapshot = baseline?.dirty_snapshot && typeof baseline.dirty_snapshot === 'object' && !Array.isArray(baseline.dirty_snapshot)
|
|
123
|
+
? baseline.dirty_snapshot
|
|
124
|
+
: {};
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
for (const filePath of targetFiles) {
|
|
128
|
+
const baselineMarker = baselineSnapshot[filePath];
|
|
129
|
+
const currentMarker = getWorkspaceFileMarker(root, filePath);
|
|
130
|
+
|
|
131
|
+
if (baselineMarker && currentMarker !== baselineMarker) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
restored_files: restoredFiles.sort(),
|
|
135
|
+
cleanup_error: `verification.produced_files declared "${filePath}" as ignore, but that path was already dirty at dispatch and cannot be restored safely.`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (baselineMarker) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
restoreCleanPath(root, filePath);
|
|
144
|
+
restoredFiles.push(filePath);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const remainingDirty = new Set(collectDirtyActorFiles(root));
|
|
148
|
+
const stranded = targetFiles.filter((filePath) => remainingDirty.has(filePath));
|
|
149
|
+
if (stranded.length > 0) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
restored_files: restoredFiles.sort(),
|
|
153
|
+
cleanup_error: `Ignored verification-produced files remain dirty after cleanup: ${stranded.join(', ')}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
restored_files: restoredFiles.sort(),
|
|
160
|
+
cleanup_error: null,
|
|
161
|
+
};
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
restored_files: restoredFiles.sort(),
|
|
166
|
+
cleanup_error: err?.message || String(err),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
84
171
|
function createReplayWorkspaceGuard(root) {
|
|
85
172
|
if (!isGitRepo(root)) {
|
|
86
173
|
return {
|