agentxchain 2.147.0 → 2.149.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.
@@ -57,6 +57,14 @@ function normalizeFilesChanged(filesChanged) {
57
57
  return normalizeCheckpointableFiles(filesChanged);
58
58
  }
59
59
 
60
+ function normalizeGitBaselineRef(ref) {
61
+ if (typeof ref !== 'string' || !ref.startsWith('git:')) {
62
+ return null;
63
+ }
64
+ const gitRef = ref.slice(4).trim();
65
+ return gitRef || null;
66
+ }
67
+
60
68
  function supportsLegacyFilesChangedRecovery(entry) {
61
69
  const artifactType = entry?.artifact?.type;
62
70
  return artifactType === 'workspace' || artifactType === 'patch';
@@ -139,6 +147,85 @@ function buildCheckpointCommit(entry) {
139
147
  return { subject, body: bodyLines.join('\n') };
140
148
  }
141
149
 
150
+ function diffMissingDeclaredPaths(declaredFiles, stagedFiles) {
151
+ const stagedSet = new Set((Array.isArray(stagedFiles) ? stagedFiles : []).map((value) => value.trim()).filter(Boolean));
152
+ return (Array.isArray(declaredFiles) ? declaredFiles : []).filter((filePath) => !stagedSet.has(filePath));
153
+ }
154
+
155
+ /**
156
+ * Partition paths that were missing from the staged-diff into
157
+ * (a) paths genuinely absent from git (untracked or dirty without staging)
158
+ * (b) paths already committed upstream (tracked in HEAD, no pending diff)
159
+ * (c) paths tracked and clean at HEAD but unchanged since the accepted
160
+ * baseline — the BUG-55A wrong-lineage case (actor committed the file
161
+ * off the accepted lineage, e.g. on a throwaway side-branch or the
162
+ * lineage was reset to baseline).
163
+ *
164
+ * BUG-55A completeness must fail on (a) and (c). (c) has a different
165
+ * operator-visible recovery path than (a): the file DID exist in some
166
+ * commit but is not reachable from the accepted baseline + HEAD comparison
167
+ * the governed run relies on. Surface it separately so the CLI can tell
168
+ * the operator which failure they hit.
169
+ */
170
+ function partitionDeclaredPathsByUpstreamPresence(root, missingPaths, options = {}) {
171
+ const baselineRef = normalizeGitBaselineRef(options.baselineRef);
172
+ const genuinelyMissing = [];
173
+ const divergentFromAcceptedLineage = [];
174
+ const alreadyCommittedUpstream = [];
175
+ for (const filePath of missingPaths) {
176
+ let tracked = false;
177
+ try {
178
+ git(root, ['ls-files', '--error-unmatch', '--', filePath]);
179
+ tracked = true;
180
+ } catch {
181
+ tracked = false;
182
+ }
183
+ if (!tracked) {
184
+ genuinelyMissing.push(filePath);
185
+ continue;
186
+ }
187
+ let hasDivergence = false;
188
+ try {
189
+ const headDiff = git(root, ['diff', 'HEAD', '--', filePath]);
190
+ const cachedDiff = git(root, ['diff', '--cached', '--', filePath]);
191
+ hasDivergence = Boolean(headDiff) || Boolean(cachedDiff);
192
+ } catch {
193
+ hasDivergence = true;
194
+ }
195
+ if (hasDivergence) {
196
+ genuinelyMissing.push(filePath);
197
+ continue;
198
+ }
199
+
200
+ // BUG-55A wrong-branch guard: a path only counts as already checkpointed
201
+ // if the current branch differs from the accepted baseline on that path.
202
+ if (baselineRef) {
203
+ let changedSinceAcceptedBaseline = false;
204
+ try {
205
+ const baselineDiff = git(root, ['diff', baselineRef, 'HEAD', '--', filePath]);
206
+ changedSinceAcceptedBaseline = Boolean(baselineDiff);
207
+ } catch {
208
+ changedSinceAcceptedBaseline = true;
209
+ }
210
+ if (!changedSinceAcceptedBaseline) {
211
+ divergentFromAcceptedLineage.push(filePath);
212
+ continue;
213
+ }
214
+ }
215
+
216
+ alreadyCommittedUpstream.push(filePath);
217
+ }
218
+ // Preserve the pre-existing return shape: `genuinelyMissing` is the union
219
+ // that the completeness gate must fail on. `divergent_from_accepted_lineage`
220
+ // is an additional, operator-facing subcategory that callers can surface
221
+ // without changing the pass/fail contract.
222
+ return {
223
+ genuinelyMissing: [...genuinelyMissing, ...divergentFromAcceptedLineage],
224
+ divergentFromAcceptedLineage,
225
+ alreadyCommittedUpstream,
226
+ };
227
+ }
228
+
142
229
  export function detectPendingCheckpoint(root, dirtyFiles = []) {
143
230
  const actorDirtyFiles = normalizeFilesChanged(dirtyFiles);
144
231
  if (actorDirtyFiles.length === 0) return { required: false };
@@ -233,12 +320,36 @@ export function checkpointAcceptedTurn(root, opts = {}) {
233
320
  };
234
321
  }
235
322
 
323
+ const rawMissingFromStage = diffMissingDeclaredPaths(filesChanged, staged);
324
+ const { genuinelyMissing, divergentFromAcceptedLineage, alreadyCommittedUpstream } =
325
+ partitionDeclaredPathsByUpstreamPresence(root, rawMissingFromStage, {
326
+ baselineRef: entry?.observed_artifact?.baseline_ref ?? null,
327
+ });
328
+ if (genuinelyMissing.length > 0) {
329
+ const baseMessage = `Checkpoint completeness failure: accepted turn ${entry.turn_id} declared ${filesChanged.length} checkpointable file(s), but Git staged only ${staged.length} and ${genuinelyMissing.length} declared path(s) are absent from git. Missing from checkpoint: ${genuinelyMissing.join(', ')}.`;
330
+ const lineageHint = divergentFromAcceptedLineage.length > 0
331
+ ? ` Wrong-lineage paths (tracked at HEAD but unchanged since accepted baseline — actor likely committed off the accepted lineage): ${divergentFromAcceptedLineage.join(', ')}.`
332
+ : '';
333
+ return {
334
+ ok: false,
335
+ turn: entry,
336
+ error: `${baseMessage}${lineageHint}`,
337
+ missing_declared_paths: genuinelyMissing,
338
+ divergent_from_accepted_lineage: divergentFromAcceptedLineage,
339
+ already_committed_upstream: alreadyCommittedUpstream,
340
+ staged_paths: staged,
341
+ };
342
+ }
343
+
236
344
  if (staged.length === 0) {
237
345
  return {
238
346
  ok: true,
239
347
  skipped: true,
240
348
  turn: entry,
241
- reason: `Accepted turn ${entry.turn_id} has no staged repo changes to checkpoint.`,
349
+ reason: alreadyCommittedUpstream.length > 0
350
+ ? `Accepted turn ${entry.turn_id} has no staged repo changes to checkpoint; all ${alreadyCommittedUpstream.length} declared file(s) already present in HEAD.`
351
+ : `Accepted turn ${entry.turn_id} has no staged repo changes to checkpoint.`,
352
+ already_committed_upstream: alreadyCommittedUpstream,
242
353
  };
243
354
  }
244
355
 
@@ -600,6 +600,15 @@ function validateVerification(tr) {
600
600
  }
601
601
  }
602
602
 
603
+ if (Array.isArray(v.commands)) {
604
+ for (let i = 0; i < v.commands.length; i++) {
605
+ const command = v.commands[i];
606
+ if (typeof command !== 'string' || command.trim().length === 0) {
607
+ errors.push(`verification.commands[${i}] must be a non-empty string.`);
608
+ }
609
+ }
610
+ }
611
+
603
612
  // machine_evidence exit codes should be consistent with status
604
613
  if (Array.isArray(v.machine_evidence)) {
605
614
  for (let i = 0; i < v.machine_evidence.length; i++) {
@@ -608,8 +617,8 @@ function validateVerification(tr) {
608
617
  errors.push(`verification.machine_evidence[${i}] must be an object.`);
609
618
  continue;
610
619
  }
611
- if (typeof entry.command !== 'string') {
612
- errors.push(`verification.machine_evidence[${i}].command must be a string.`);
620
+ if (typeof entry.command !== 'string' || entry.command.trim().length === 0) {
621
+ errors.push(`verification.machine_evidence[${i}].command must be a non-empty string.`);
613
622
  }
614
623
  if (typeof entry.exit_code !== 'number' || !Number.isInteger(entry.exit_code)) {
615
624
  errors.push(`verification.machine_evidence[${i}].exit_code must be an integer.`);