agentxchain 2.140.0 → 2.141.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.140.0",
3
+ "version": "2.141.1",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -153,7 +153,7 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
153
153
  test/release-notes-gate.test.js
154
154
  test/release-identity-hardening.test.js
155
155
  test/normalized-config.test.js
156
- test/conformance.test.js
156
+ test/protocol-conformance.test.js
157
157
  test/beta-scenario-emission-guard.test.js
158
158
  test/claim-reality-preflight.test.js
159
159
  test/beta-tester-scenarios/*.test.js
@@ -37,6 +37,7 @@ import {
37
37
  deriveAcceptedRef,
38
38
  checkCleanBaseline,
39
39
  isOperationalPath,
40
+ isBaselineExemptPath,
40
41
  } from './repo-observer.js';
41
42
  import { getMaxConcurrentTurns } from './normalized-config.js';
42
43
  import { getTurnStagingResultPath, getTurnStagingDir, getDispatchTurnDir, getReviewArtifactPath } from './turn-paths.js';
@@ -106,6 +107,34 @@ function buildInitialPhaseGateStatus(config) {
106
107
  );
107
108
  }
108
109
 
110
+ function buildObservationDrift(beforeObservation, afterObservation) {
111
+ const beforeFiles = (Array.isArray(beforeObservation?.files_changed) ? beforeObservation.files_changed : [])
112
+ .filter((filePath) => !isBaselineExemptPath(filePath));
113
+ const afterFiles = (Array.isArray(afterObservation?.files_changed) ? afterObservation.files_changed : [])
114
+ .filter((filePath) => !isBaselineExemptPath(filePath));
115
+ const beforeSet = new Set(beforeFiles);
116
+ const afterSet = new Set(afterFiles);
117
+ const beforeMarkers = beforeObservation?.file_markers && typeof beforeObservation.file_markers === 'object'
118
+ ? beforeObservation.file_markers
119
+ : {};
120
+ const afterMarkers = afterObservation?.file_markers && typeof afterObservation.file_markers === 'object'
121
+ ? afterObservation.file_markers
122
+ : {};
123
+
124
+ const added = afterFiles.filter((filePath) => !beforeSet.has(filePath));
125
+ const removed = beforeFiles.filter((filePath) => !afterSet.has(filePath));
126
+ const markerChanged = beforeFiles.filter((filePath) => (
127
+ afterSet.has(filePath) && beforeMarkers[filePath] !== afterMarkers[filePath]
128
+ ));
129
+
130
+ return {
131
+ added,
132
+ removed,
133
+ marker_changed: markerChanged,
134
+ drifted_files: [...new Set([...added, ...removed, ...markerChanged])].sort(),
135
+ };
136
+ }
137
+
109
138
  function listIntakeIntentFiles(root) {
110
139
  const intentsDir = join(root, INTAKE_INTENTS_DIR);
111
140
  if (!existsSync(intentsDir)) return [];
@@ -3210,11 +3239,17 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3210
3239
  currentTurn,
3211
3240
  historyEntries,
3212
3241
  );
3213
- const observation = attributeObservedChangesToTurn(rawObservation, currentTurn, historyEntries, {
3242
+ const observationAttributionOptions = {
3214
3243
  currentDeclaredFiles: turnResult.files_changed || [],
3215
3244
  concurrentSiblingIds: pendingConcurrentSiblingDeclarations.map((entry) => entry.turn_id),
3216
3245
  pendingConcurrentSiblingDeclarations,
3217
- });
3246
+ };
3247
+ const observation = attributeObservedChangesToTurn(
3248
+ rawObservation,
3249
+ currentTurn,
3250
+ historyEntries,
3251
+ observationAttributionOptions,
3252
+ );
3218
3253
  const role = config.roles?.[turnResult.role];
3219
3254
  const runtimeId = turnResult.runtime_id;
3220
3255
  const runtime = config.runtimes?.[runtimeId];
@@ -3403,6 +3438,30 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3403
3438
  ? replayVerificationMachineEvidence({ root, verification: turnResult.verification })
3404
3439
  : null;
3405
3440
 
3441
+ if (verificationReplay?.workspace_guard?.ok === false) {
3442
+ const replayError = verificationReplay.workspace_guard.cleanup_error
3443
+ || 'Verification replay mutated actor-owned workspace files and cleanup failed.';
3444
+ transitionToFailedAcceptance(root, state, currentTurn, replayError, {
3445
+ error_code: 'verification_replay_drift',
3446
+ stage: 'verification_replay',
3447
+ extra: {
3448
+ replay_side_effect_files: verificationReplay.workspace_guard.restored_files || [],
3449
+ },
3450
+ });
3451
+ return {
3452
+ ok: false,
3453
+ error: replayError,
3454
+ validation: {
3455
+ ...validation,
3456
+ ok: false,
3457
+ stage: 'verification_replay',
3458
+ error_class: 'verification_replay_error',
3459
+ errors: [replayError],
3460
+ warnings: validation.warnings,
3461
+ },
3462
+ };
3463
+ }
3464
+
3406
3465
  // Policy evaluation — declarative governance rules (spec: POLICY_ENGINE_SPEC.md)
3407
3466
  const policyResult = evaluatePolicies(config.policies || [], {
3408
3467
  currentPhase: state.phase,
@@ -3638,6 +3697,38 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3638
3697
  }
3639
3698
  }
3640
3699
 
3700
+ const finalObservation = attributeObservedChangesToTurn(
3701
+ observeChanges(root, baseline),
3702
+ currentTurn,
3703
+ historyEntries,
3704
+ observationAttributionOptions,
3705
+ );
3706
+ const observationDrift = buildObservationDrift(observation, finalObservation);
3707
+ if (observationDrift.drifted_files.length > 0) {
3708
+ const driftError = `Acceptance mutated actor-owned workspace after artifact observation: ${observationDrift.drifted_files.join(', ')}`;
3709
+ transitionToFailedAcceptance(root, state, currentTurn, driftError, {
3710
+ error_code: 'acceptance_drift',
3711
+ stage: 'artifact_observation',
3712
+ extra: {
3713
+ added_files: observationDrift.added,
3714
+ removed_files: observationDrift.removed,
3715
+ marker_changed_files: observationDrift.marker_changed,
3716
+ },
3717
+ });
3718
+ return {
3719
+ ok: false,
3720
+ error: driftError,
3721
+ validation: {
3722
+ ...validation,
3723
+ ok: false,
3724
+ stage: 'artifact_observation',
3725
+ error_class: 'artifact_error',
3726
+ errors: [driftError],
3727
+ warnings: validation.warnings,
3728
+ },
3729
+ };
3730
+ }
3731
+
3641
3732
  const acceptedSequence = (state.turn_sequence || 0) + 1;
3642
3733
  const historyEntry = {
3643
3734
  turn_id: turnResult.turn_id,
@@ -29,6 +29,8 @@ const OPERATIONAL_PATH_PREFIXES = [
29
29
  '.agentxchain/intake/',
30
30
  '.agentxchain/locks/',
31
31
  '.agentxchain/transactions/',
32
+ '.agentxchain/missions/',
33
+ '.agentxchain/multirepo/',
32
34
  ];
33
35
 
34
36
  // Orchestrator-owned state files that agents must never be blamed for modifying.
@@ -38,6 +40,7 @@ const ORCHESTRATOR_STATE_FILES = [
38
40
  '.agentxchain/session.json',
39
41
  '.agentxchain/history.jsonl',
40
42
  '.agentxchain/decision-ledger.jsonl',
43
+ '.agentxchain/repo-decisions.jsonl',
41
44
  '.agentxchain/lock.json',
42
45
  '.agentxchain/hook-audit.jsonl',
43
46
  '.agentxchain/hook-annotations.jsonl',
@@ -71,7 +74,7 @@ export function isOperationalPath(filePath) {
71
74
  || ORCHESTRATOR_STATE_FILES.includes(filePath);
72
75
  }
73
76
 
74
- function isBaselineExemptPath(filePath) {
77
+ export function isBaselineExemptPath(filePath) {
75
78
  return isOperationalPath(filePath)
76
79
  || BASELINE_EXEMPT_PATH_PREFIXES.some(prefix => filePath.startsWith(prefix));
77
80
  }
@@ -1,4 +1,10 @@
1
- import { spawnSync } from 'node:child_process';
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ import { isOperationalPath } from './repo-observer.js';
2
8
 
3
9
  export const DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS = 30_000;
4
10
 
@@ -22,10 +28,15 @@ export function replayVerificationMachineEvidence({ root, verification, timeoutM
22
28
  return payload;
23
29
  }
24
30
 
25
- payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
26
- payload.replayed_commands = payload.commands.length;
27
- payload.matched_commands = payload.commands.filter((entry) => entry.matched).length;
28
- payload.overall = payload.commands.every((entry) => entry.matched) ? 'match' : 'mismatch';
31
+ const workspaceGuard = createReplayWorkspaceGuard(root);
32
+ try {
33
+ payload.commands = machineEvidence.map((entry, index) => replayEvidenceCommand(root, entry, index, timeoutMs));
34
+ payload.replayed_commands = payload.commands.length;
35
+ payload.matched_commands = payload.commands.filter((entry) => entry.matched).length;
36
+ payload.overall = payload.commands.every((entry) => entry.matched) ? 'match' : 'mismatch';
37
+ } finally {
38
+ payload.workspace_guard = workspaceGuard.cleanup();
39
+ }
29
40
 
30
41
  return payload;
31
42
  }
@@ -69,3 +80,186 @@ export function summarizeVerificationReplay(payload) {
69
80
  ...(payload.reason ? { reason: payload.reason } : {}),
70
81
  };
71
82
  }
83
+
84
+ function createReplayWorkspaceGuard(root) {
85
+ if (!isGitRepo(root)) {
86
+ return {
87
+ cleanup() {
88
+ return { ok: true, restored_files: [], cleanup_error: null };
89
+ },
90
+ };
91
+ }
92
+
93
+ const backupDir = mkdtempSync(join(tmpdir(), 'axc-verification-replay-'));
94
+ const beforeState = captureDirtyWorkspaceState(root, backupDir);
95
+
96
+ return {
97
+ cleanup() {
98
+ const restoredFiles = new Set();
99
+
100
+ try {
101
+ const afterFiles = collectDirtyActorFiles(root);
102
+ const candidateFiles = new Set([...beforeState.files.keys(), ...afterFiles]);
103
+
104
+ for (const filePath of candidateFiles) {
105
+ const beforeInfo = beforeState.files.get(filePath);
106
+ const currentMarker = getWorkspaceFileMarker(root, filePath);
107
+
108
+ if (beforeInfo) {
109
+ if (currentMarker !== beforeInfo.marker) {
110
+ restorePreReplayPath(root, filePath, beforeInfo);
111
+ restoredFiles.add(filePath);
112
+ }
113
+ continue;
114
+ }
115
+
116
+ restoreCleanPath(root, filePath);
117
+ restoredFiles.add(filePath);
118
+ }
119
+
120
+ const remainingDrift = detectWorkspaceDrift(root, beforeState.files);
121
+ if (remainingDrift.length > 0) {
122
+ return {
123
+ ok: false,
124
+ restored_files: [...restoredFiles].sort(),
125
+ cleanup_error: `Verification replay left actor-owned workspace drift after cleanup: ${remainingDrift.join(', ')}`,
126
+ };
127
+ }
128
+
129
+ return {
130
+ ok: true,
131
+ restored_files: [...restoredFiles].sort(),
132
+ cleanup_error: null,
133
+ };
134
+ } catch (err) {
135
+ return {
136
+ ok: false,
137
+ restored_files: [...restoredFiles].sort(),
138
+ cleanup_error: err?.message || String(err),
139
+ };
140
+ } finally {
141
+ rmSync(backupDir, { recursive: true, force: true });
142
+ }
143
+ },
144
+ };
145
+ }
146
+
147
+ function captureDirtyWorkspaceState(root, backupDir) {
148
+ const files = new Map();
149
+ for (const filePath of collectDirtyActorFiles(root)) {
150
+ const absPath = join(root, filePath);
151
+ const backupPath = join(backupDir, filePath);
152
+ const existed = existsSync(absPath);
153
+ const marker = getWorkspaceFileMarker(root, filePath);
154
+
155
+ if (existed) {
156
+ mkdirSync(dirname(backupPath), { recursive: true });
157
+ cpSync(absPath, backupPath, { force: true, recursive: true });
158
+ }
159
+
160
+ files.set(filePath, { existed, marker, backupPath });
161
+ }
162
+ return { files };
163
+ }
164
+
165
+ function detectWorkspaceDrift(root, beforeFiles) {
166
+ const afterFiles = collectDirtyActorFiles(root);
167
+ const candidates = new Set([...beforeFiles.keys(), ...afterFiles]);
168
+ const drift = [];
169
+
170
+ for (const filePath of candidates) {
171
+ const beforeInfo = beforeFiles.get(filePath);
172
+ const currentMarker = getWorkspaceFileMarker(root, filePath);
173
+ if (beforeInfo) {
174
+ if (currentMarker !== beforeInfo.marker) {
175
+ drift.push(filePath);
176
+ }
177
+ continue;
178
+ }
179
+ drift.push(filePath);
180
+ }
181
+
182
+ return drift.sort();
183
+ }
184
+
185
+ function restorePreReplayPath(root, filePath, beforeInfo) {
186
+ const absPath = join(root, filePath);
187
+ if (!beforeInfo.existed) {
188
+ rmSync(absPath, { recursive: true, force: true });
189
+ return;
190
+ }
191
+
192
+ rmSync(absPath, { recursive: true, force: true });
193
+ mkdirSync(dirname(absPath), { recursive: true });
194
+ cpSync(beforeInfo.backupPath, absPath, { force: true, recursive: true });
195
+ }
196
+
197
+ function restoreCleanPath(root, filePath) {
198
+ const absPath = join(root, filePath);
199
+ if (isTrackedPath(root, filePath)) {
200
+ execFileSync('git', ['restore', '--source=HEAD', '--staged', '--worktree', '--', filePath], {
201
+ cwd: root,
202
+ stdio: ['ignore', 'pipe', 'pipe'],
203
+ });
204
+ return;
205
+ }
206
+
207
+ rmSync(absPath, { recursive: true, force: true });
208
+ }
209
+
210
+ function collectDirtyActorFiles(root) {
211
+ return [...new Set([
212
+ ...readGitLines(root, ['diff', '--name-only', 'HEAD']),
213
+ ...readGitLines(root, ['diff', '--name-only', '--cached']),
214
+ ...readGitLines(root, ['ls-files', '--others', '--exclude-standard']),
215
+ ])]
216
+ .filter((filePath) => filePath && !isOperationalPath(filePath))
217
+ .sort();
218
+ }
219
+
220
+ function readGitLines(root, args) {
221
+ try {
222
+ const output = execFileSync('git', args, {
223
+ cwd: root,
224
+ encoding: 'utf8',
225
+ stdio: ['ignore', 'pipe', 'pipe'],
226
+ }).trim();
227
+ return output ? output.split('\n').filter(Boolean) : [];
228
+ } catch {
229
+ return [];
230
+ }
231
+ }
232
+
233
+ function isTrackedPath(root, filePath) {
234
+ try {
235
+ execFileSync('git', ['ls-files', '--error-unmatch', '--', filePath], {
236
+ cwd: root,
237
+ stdio: ['ignore', 'pipe', 'pipe'],
238
+ });
239
+ return true;
240
+ } catch {
241
+ return false;
242
+ }
243
+ }
244
+
245
+ function isGitRepo(root) {
246
+ try {
247
+ execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
248
+ cwd: root,
249
+ stdio: ['ignore', 'pipe', 'pipe'],
250
+ });
251
+ return true;
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+
257
+ function getWorkspaceFileMarker(root, filePath) {
258
+ const absPath = join(root, filePath);
259
+ if (!existsSync(absPath)) {
260
+ return '__deleted__';
261
+ }
262
+
263
+ const content = readFileSync(absPath);
264
+ return createHash('sha1').update(content).digest('hex');
265
+ }