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.
- package/bin/agentxchain.js +12 -2
- package/package.json +1 -1
- package/src/commands/audit.js +7 -1
- package/src/commands/decisions.js +94 -0
- package/src/commands/doctor.js +19 -1
- package/src/commands/report.js +7 -1
- package/src/commands/status.js +14 -0
- package/src/lib/adapters/local-cli-adapter.js +1 -1
- package/src/lib/dispatch-bundle.js +9 -0
- package/src/lib/export.js +60 -0
- package/src/lib/governed-state.js +135 -3
- package/src/lib/repo-decisions.js +100 -0
- package/src/lib/repo-observer.js +39 -5
- package/src/lib/report.js +618 -0
- package/src/lib/run-loop.js +16 -0
- package/src/lib/schemas/turn-result.schema.json +10 -0
- package/src/lib/turn-result-validator.js +13 -0
|
@@ -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 };
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -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
|
-
&&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(', ')}`);
|