agentxchain 2.91.0 → 2.93.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.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Repo Decisions — cross-run decision carryover.
3
+ *
4
+ * Decisions with durability: "repo" persist in `.agentxchain/repo-decisions.jsonl`
5
+ * across governed runs. They act as binding constraints: agents in future runs
6
+ * must comply with active repo decisions or explicitly override them.
7
+ *
8
+ * DEC-SPEC: .planning/CROSS_RUN_DECISION_CARRYOVER_SPEC.md
9
+ */
10
+
11
+ import { readFileSync, appendFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
12
+ import { join, dirname } from 'path';
13
+
14
+ const REPO_DECISIONS_PATH = '.agentxchain/repo-decisions.jsonl';
15
+
16
+ // ── Read ────────────────────────────────────────────────────────────────────
17
+
18
+ export function readRepoDecisions(root) {
19
+ const filePath = join(root, REPO_DECISIONS_PATH);
20
+ if (!existsSync(filePath)) return [];
21
+ try {
22
+ const content = readFileSync(filePath, 'utf8').trim();
23
+ if (!content) return [];
24
+ return content.split('\n').filter(Boolean).map(line => {
25
+ try { return JSON.parse(line); } catch { return null; }
26
+ }).filter(Boolean);
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+
32
+ export function getActiveRepoDecisions(root) {
33
+ return readRepoDecisions(root).filter(d => d.status === 'active');
34
+ }
35
+
36
+ export function getRepoDecisionById(root, decisionId) {
37
+ return readRepoDecisions(root).find(d => d.id === decisionId) || null;
38
+ }
39
+
40
+ // ── Write ───────────────────────────────────────────────────────────────────
41
+
42
+ export function appendRepoDecision(root, entry) {
43
+ const filePath = join(root, REPO_DECISIONS_PATH);
44
+ const dir = dirname(filePath);
45
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
46
+ appendFileSync(filePath, JSON.stringify(entry) + '\n');
47
+ }
48
+
49
+ export function overrideRepoDecision(root, targetId, overridingId) {
50
+ const all = readRepoDecisions(root);
51
+ const updated = all.map(d => {
52
+ if (d.id === targetId) {
53
+ return { ...d, status: 'overridden', overridden_by: overridingId };
54
+ }
55
+ return d;
56
+ });
57
+ const filePath = join(root, REPO_DECISIONS_PATH);
58
+ const dir = dirname(filePath);
59
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
60
+ writeFileSync(filePath, updated.map(d => JSON.stringify(d)).join('\n') + '\n');
61
+ }
62
+
63
+ // ── Validate Override ───────────────────────────────────────────────────────
64
+
65
+ export function validateOverride(root, decision) {
66
+ if (!decision.overrides) return { ok: true };
67
+ const targetId = decision.overrides;
68
+ const target = getRepoDecisionById(root, targetId);
69
+ if (!target) {
70
+ return { ok: false, error: `decisions: overrides references ${targetId} which does not exist in repo decisions.` };
71
+ }
72
+ if (target.status === 'overridden') {
73
+ return { ok: false, error: `decisions: ${targetId} is already overridden by ${target.overridden_by}.` };
74
+ }
75
+ if (target.status !== 'active') {
76
+ return { ok: false, error: `decisions: ${targetId} has status "${target.status}", only active repo decisions can be overridden.` };
77
+ }
78
+ return { ok: true };
79
+ }
80
+
81
+ // ── Render ──────────────────────────────────────────────────────────────────
82
+
83
+ export function renderRepoDecisionsMarkdown(activeDecisions) {
84
+ if (!activeDecisions || activeDecisions.length === 0) return '';
85
+ const lines = [
86
+ '## Active Repo Decisions',
87
+ '',
88
+ 'These decisions persist from prior governed runs. Comply or explicitly override with rationale.',
89
+ '',
90
+ ];
91
+ for (const d of activeDecisions) {
92
+ lines.push(`- **${d.id}** (${d.category}): ${d.statement}`);
93
+ }
94
+ lines.push('');
95
+ return lines.join('\n');
96
+ }
97
+
98
+ // ── Constants ───────────────────────────────────────────────────────────────
99
+
100
+ export { REPO_DECISIONS_PATH };
@@ -175,7 +175,7 @@ export function observeChanges(root, baseline) {
175
175
  };
176
176
  }
177
177
 
178
- export function attributeObservedChangesToTurn(observation, currentTurn, historyEntries = []) {
178
+ export function attributeObservedChangesToTurn(observation, currentTurn, historyEntries = [], options = {}) {
179
179
  const observedFiles = Array.isArray(observation?.files_changed) ? observation.files_changed : [];
180
180
  if (observedFiles.length === 0) {
181
181
  return observation;
@@ -184,6 +184,11 @@ export function attributeObservedChangesToTurn(observation, currentTurn, history
184
184
  const concurrentIds = new Set(
185
185
  Array.isArray(currentTurn?.concurrent_with) ? currentTurn.concurrent_with : [],
186
186
  );
187
+ for (const siblingId of Array.isArray(options.concurrentSiblingIds) ? options.concurrentSiblingIds : []) {
188
+ if (typeof siblingId === 'string' && siblingId.length > 0) {
189
+ concurrentIds.add(siblingId);
190
+ }
191
+ }
187
192
  if (concurrentIds.size === 0) {
188
193
  return observation;
189
194
  }
@@ -195,11 +200,17 @@ export function attributeObservedChangesToTurn(observation, currentTurn, history
195
200
  .filter((entry) => (
196
201
  Number.isInteger(entry?.accepted_sequence)
197
202
  && entry.accepted_sequence > assignedSequence
198
- && concurrentIds.has(entry.turn_id)
203
+ && (
204
+ concurrentIds.has(entry.turn_id)
205
+ || (Array.isArray(entry?.concurrent_with) && entry.concurrent_with.includes(currentTurn?.turn_id))
206
+ )
199
207
  ))
200
208
  .sort((left, right) => left.accepted_sequence - right.accepted_sequence);
201
209
 
202
- if (acceptedConcurrentSiblings.length === 0) {
210
+ const pendingConcurrentSiblingDeclarations = Array.isArray(options.pendingConcurrentSiblingDeclarations)
211
+ ? options.pendingConcurrentSiblingDeclarations
212
+ : [];
213
+ if (acceptedConcurrentSiblings.length === 0 && pendingConcurrentSiblingDeclarations.length === 0) {
203
214
  return observation;
204
215
  }
205
216
 
@@ -221,7 +232,20 @@ export function attributeObservedChangesToTurn(observation, currentTurn, history
221
232
  }
222
233
  }
223
234
 
224
- if (siblingMarkersByFile.size === 0) {
235
+ const currentDeclaredFiles = new Set(
236
+ Array.isArray(options.currentDeclaredFiles) ? options.currentDeclaredFiles : [],
237
+ );
238
+ const pendingConcurrentSiblingFiles = new Set();
239
+ for (const declaration of pendingConcurrentSiblingDeclarations) {
240
+ const siblingFiles = Array.isArray(declaration?.files_changed) ? declaration.files_changed : [];
241
+ for (const filePath of siblingFiles) {
242
+ if (typeof filePath === 'string' && filePath.length > 0 && !currentDeclaredFiles.has(filePath)) {
243
+ pendingConcurrentSiblingFiles.add(filePath);
244
+ }
245
+ }
246
+ }
247
+
248
+ if (siblingMarkersByFile.size === 0 && pendingConcurrentSiblingFiles.size === 0) {
225
249
  return observation;
226
250
  }
227
251
 
@@ -239,6 +263,10 @@ export function attributeObservedChangesToTurn(observation, currentTurn, history
239
263
  attributedToConcurrentSiblings.push(filePath);
240
264
  continue;
241
265
  }
266
+ if (pendingConcurrentSiblingFiles.has(filePath)) {
267
+ attributedToConcurrentSiblings.push(filePath);
268
+ continue;
269
+ }
242
270
  nextFiles.push(filePath);
243
271
  if (typeof currentMarker === 'string') {
244
272
  nextMarkers[filePath] = currentMarker;
@@ -503,7 +531,13 @@ export function compareDeclaredVsObserved(declared, observed, writeAuthority, op
503
531
 
504
532
  if (writeAuthority === 'authoritative') {
505
533
  if (undeclared.length > 0) {
506
- errors.push(`Undeclared file changes detected (observed but not in files_changed): ${undeclared.join(', ')}`);
534
+ if (options.has_unaccepted_concurrent_siblings) {
535
+ // Concurrent siblings may have written these files; downgrade to warning.
536
+ // The attribution system will handle later-accepted siblings correctly.
537
+ warnings.push(`Undeclared file changes detected (likely from concurrent sibling turns): ${undeclared.join(', ')}`);
538
+ } else {
539
+ errors.push(`Undeclared file changes detected (observed but not in files_changed): ${undeclared.join(', ')}`);
540
+ }
507
541
  }
508
542
  if (phantom.length > 0) {
509
543
  warnings.push(`Declared files not observed in actual diff: ${phantom.join(', ')}`);