agentxchain 2.46.0 → 2.47.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 +4 -1
- package/dashboard/app.js +6 -0
- package/dashboard/components/coordinator-timeouts.js +220 -0
- package/dashboard/components/timeouts.js +201 -0
- package/dashboard/index.html +2 -0
- package/package.json +1 -1
- package/scripts/publish-from-tag.sh +33 -4
- package/src/commands/history.js +41 -1
- package/src/commands/init.js +1 -0
- package/src/commands/migrate.js +1 -0
- package/src/commands/run.js +32 -1
- package/src/commands/status.js +55 -0
- package/src/lib/approval-policy.js +139 -0
- package/src/lib/blocked-state.js +11 -0
- package/src/lib/dashboard/bridge-server.js +14 -0
- package/src/lib/dashboard/coordinator-timeout-status.js +139 -0
- package/src/lib/dashboard/timeout-status.js +201 -0
- package/src/lib/export.js +2 -0
- package/src/lib/governed-state.js +428 -30
- package/src/lib/normalized-config.js +123 -0
- package/src/lib/reference-conformance-adapter.js +1 -0
- package/src/lib/repo-observer.js +132 -1
- package/src/lib/report.js +331 -6
- package/src/lib/run-history.js +111 -0
- package/src/lib/run-loop.js +9 -3
- package/src/lib/run-provenance.js +90 -0
- package/src/lib/schema.js +47 -0
- package/src/lib/timeout-evaluator.js +234 -0
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { validateHooksConfig } from './hook-runner.js';
|
|
16
16
|
import { validateNotificationsConfig } from './notification-runner.js';
|
|
17
17
|
import { validatePolicies, normalizePolicies } from './policy-evaluator.js';
|
|
18
|
+
import { validateTimeoutsConfig } from './timeout-evaluator.js';
|
|
18
19
|
import { SUPPORTED_TOKEN_COUNTER_PROVIDERS } from './token-counter.js';
|
|
19
20
|
import {
|
|
20
21
|
buildDefaultWorkflowKitArtifactsForPhase,
|
|
@@ -519,6 +520,17 @@ export function validateV4Config(data, projectRoot) {
|
|
|
519
520
|
errors.push(...policyValidation.errors);
|
|
520
521
|
}
|
|
521
522
|
|
|
523
|
+
// Approval Policy (optional but validated if present)
|
|
524
|
+
if (data.approval_policy !== undefined) {
|
|
525
|
+
errors.push(...validateApprovalPolicy(data.approval_policy, data.routing));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Timeouts (optional but validated if present)
|
|
529
|
+
if (data.timeouts !== undefined) {
|
|
530
|
+
const timeoutValidation = validateTimeoutsConfig(data.timeouts, data.routing);
|
|
531
|
+
errors.push(...timeoutValidation.errors);
|
|
532
|
+
}
|
|
533
|
+
|
|
522
534
|
return { ok: errors.length === 0, errors };
|
|
523
535
|
}
|
|
524
536
|
|
|
@@ -691,6 +703,113 @@ export function validateWorkflowKitConfig(wk, routing, roles) {
|
|
|
691
703
|
return { ok: errors.length === 0, errors, warnings };
|
|
692
704
|
}
|
|
693
705
|
|
|
706
|
+
const VALID_APPROVAL_ACTIONS = ['auto_approve', 'require_human'];
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Validate the approval_policy config section.
|
|
710
|
+
* Returns an array of error strings.
|
|
711
|
+
*/
|
|
712
|
+
export function validateApprovalPolicy(ap, routing) {
|
|
713
|
+
const errors = [];
|
|
714
|
+
if (ap === null || ap === undefined) return errors;
|
|
715
|
+
if (typeof ap !== 'object' || Array.isArray(ap)) {
|
|
716
|
+
errors.push('approval_policy must be an object');
|
|
717
|
+
return errors;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const routingPhases = routing ? Object.keys(routing) : [];
|
|
721
|
+
|
|
722
|
+
// phase_transitions
|
|
723
|
+
if (ap.phase_transitions !== undefined) {
|
|
724
|
+
const pt = ap.phase_transitions;
|
|
725
|
+
if (typeof pt !== 'object' || Array.isArray(pt)) {
|
|
726
|
+
errors.push('approval_policy.phase_transitions must be an object');
|
|
727
|
+
} else {
|
|
728
|
+
if (pt.default !== undefined && !VALID_APPROVAL_ACTIONS.includes(pt.default)) {
|
|
729
|
+
errors.push(`approval_policy.phase_transitions.default must be one of: ${VALID_APPROVAL_ACTIONS.join(', ')}`);
|
|
730
|
+
}
|
|
731
|
+
if (pt.rules !== undefined) {
|
|
732
|
+
if (!Array.isArray(pt.rules)) {
|
|
733
|
+
errors.push('approval_policy.phase_transitions.rules must be an array');
|
|
734
|
+
} else {
|
|
735
|
+
for (let i = 0; i < pt.rules.length; i++) {
|
|
736
|
+
const rule = pt.rules[i];
|
|
737
|
+
const prefix = `approval_policy.phase_transitions.rules[${i}]`;
|
|
738
|
+
if (!rule || typeof rule !== 'object') {
|
|
739
|
+
errors.push(`${prefix} must be an object`);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (!VALID_APPROVAL_ACTIONS.includes(rule.action)) {
|
|
743
|
+
errors.push(`${prefix}.action must be one of: ${VALID_APPROVAL_ACTIONS.join(', ')}`);
|
|
744
|
+
}
|
|
745
|
+
if (rule.from_phase !== undefined) {
|
|
746
|
+
if (typeof rule.from_phase !== 'string') {
|
|
747
|
+
errors.push(`${prefix}.from_phase must be a string`);
|
|
748
|
+
} else if (routingPhases.length > 0 && !routingPhases.includes(rule.from_phase)) {
|
|
749
|
+
errors.push(`${prefix}.from_phase "${rule.from_phase}" does not exist in routing`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (rule.to_phase !== undefined) {
|
|
753
|
+
if (typeof rule.to_phase !== 'string') {
|
|
754
|
+
errors.push(`${prefix}.to_phase must be a string`);
|
|
755
|
+
} else if (routingPhases.length > 0 && !routingPhases.includes(rule.to_phase)) {
|
|
756
|
+
errors.push(`${prefix}.to_phase "${rule.to_phase}" does not exist in routing`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (rule.when !== undefined) {
|
|
760
|
+
errors.push(...validateApprovalWhen(rule.when, prefix));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// run_completion
|
|
769
|
+
if (ap.run_completion !== undefined) {
|
|
770
|
+
const rc = ap.run_completion;
|
|
771
|
+
if (typeof rc !== 'object' || Array.isArray(rc)) {
|
|
772
|
+
errors.push('approval_policy.run_completion must be an object');
|
|
773
|
+
} else {
|
|
774
|
+
if (rc.action !== undefined && !VALID_APPROVAL_ACTIONS.includes(rc.action)) {
|
|
775
|
+
errors.push(`approval_policy.run_completion.action must be one of: ${VALID_APPROVAL_ACTIONS.join(', ')}`);
|
|
776
|
+
}
|
|
777
|
+
if (rc.when !== undefined) {
|
|
778
|
+
errors.push(...validateApprovalWhen(rc.when, 'approval_policy.run_completion'));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return errors;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function validateApprovalWhen(when, prefix) {
|
|
787
|
+
const errors = [];
|
|
788
|
+
if (typeof when !== 'object' || Array.isArray(when) || when === null) {
|
|
789
|
+
errors.push(`${prefix}.when must be an object`);
|
|
790
|
+
return errors;
|
|
791
|
+
}
|
|
792
|
+
if (when.gate_passed !== undefined && typeof when.gate_passed !== 'boolean') {
|
|
793
|
+
errors.push(`${prefix}.when.gate_passed must be a boolean`);
|
|
794
|
+
}
|
|
795
|
+
if (when.roles_participated !== undefined) {
|
|
796
|
+
if (!Array.isArray(when.roles_participated)) {
|
|
797
|
+
errors.push(`${prefix}.when.roles_participated must be an array of role IDs`);
|
|
798
|
+
} else {
|
|
799
|
+
for (const r of when.roles_participated) {
|
|
800
|
+
if (typeof r !== 'string' || !r.trim()) {
|
|
801
|
+
errors.push(`${prefix}.when.roles_participated entries must be non-empty strings`);
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (when.all_phases_visited !== undefined && typeof when.all_phases_visited !== 'boolean') {
|
|
808
|
+
errors.push(`${prefix}.when.all_phases_visited must be a boolean`);
|
|
809
|
+
}
|
|
810
|
+
return errors;
|
|
811
|
+
}
|
|
812
|
+
|
|
694
813
|
/**
|
|
695
814
|
* Normalize a legacy v3 config into the internal shape.
|
|
696
815
|
* Does NOT modify the original file — this is a read-time transformation.
|
|
@@ -733,6 +852,8 @@ export function normalizeV3(raw) {
|
|
|
733
852
|
notifications: {},
|
|
734
853
|
budget: null,
|
|
735
854
|
policies: [],
|
|
855
|
+
approval_policy: null,
|
|
856
|
+
timeouts: null,
|
|
736
857
|
workflow_kit: normalizeWorkflowKit(undefined, DEFAULT_PHASES),
|
|
737
858
|
retention: {
|
|
738
859
|
talk_strategy: 'append_only',
|
|
@@ -798,6 +919,8 @@ export function normalizeV4(raw) {
|
|
|
798
919
|
notifications: raw.notifications || {},
|
|
799
920
|
budget: raw.budget || null,
|
|
800
921
|
policies: normalizePolicies(raw.policies),
|
|
922
|
+
approval_policy: raw.approval_policy || null,
|
|
923
|
+
timeouts: raw.timeouts || null,
|
|
801
924
|
workflow_kit: normalizeWorkflowKit(raw.workflow_kit, routingPhases),
|
|
802
925
|
retention: raw.retention || {
|
|
803
926
|
talk_strategy: 'append_only',
|
|
@@ -139,6 +139,7 @@ function inflateState(rawState = {}, config) {
|
|
|
139
139
|
blocked_on: rawState.blocked_on ?? null,
|
|
140
140
|
blocked_reason: rawState.blocked_reason ?? null,
|
|
141
141
|
escalation: rawState.escalation ?? null,
|
|
142
|
+
last_gate_failure: rawState.last_gate_failure ?? null,
|
|
142
143
|
accepted_sequence: rawState.accepted_sequence ?? 0,
|
|
143
144
|
turn_sequence: rawState.turn_sequence ?? 0,
|
|
144
145
|
budget_reservations: rawState.budget_reservations ?? {},
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -121,6 +121,7 @@ export function observeChanges(root, baseline) {
|
|
|
121
121
|
// Non-git project — no observation possible
|
|
122
122
|
return {
|
|
123
123
|
files_changed: [],
|
|
124
|
+
file_markers: {},
|
|
124
125
|
head_ref: null,
|
|
125
126
|
diff_summary: null,
|
|
126
127
|
observation_available: false,
|
|
@@ -162,6 +163,7 @@ export function observeChanges(root, baseline) {
|
|
|
162
163
|
|
|
163
164
|
return {
|
|
164
165
|
files_changed: actorFiles.sort(),
|
|
166
|
+
file_markers: buildFileMarkers(root, actorFiles),
|
|
165
167
|
head_ref: currentHead,
|
|
166
168
|
diff_summary: diffSummary,
|
|
167
169
|
observation_available: true,
|
|
@@ -169,6 +171,119 @@ export function observeChanges(root, baseline) {
|
|
|
169
171
|
};
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
export function attributeObservedChangesToTurn(observation, currentTurn, historyEntries = []) {
|
|
175
|
+
const observedFiles = Array.isArray(observation?.files_changed) ? observation.files_changed : [];
|
|
176
|
+
if (observedFiles.length === 0) {
|
|
177
|
+
return observation;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const concurrentIds = new Set(
|
|
181
|
+
Array.isArray(currentTurn?.concurrent_with) ? currentTurn.concurrent_with : [],
|
|
182
|
+
);
|
|
183
|
+
if (concurrentIds.size === 0) {
|
|
184
|
+
return observation;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const assignedSequence = Number.isInteger(currentTurn?.assigned_sequence)
|
|
188
|
+
? currentTurn.assigned_sequence
|
|
189
|
+
: 0;
|
|
190
|
+
const acceptedConcurrentSiblings = (Array.isArray(historyEntries) ? historyEntries : [])
|
|
191
|
+
.filter((entry) => (
|
|
192
|
+
Number.isInteger(entry?.accepted_sequence)
|
|
193
|
+
&& entry.accepted_sequence > assignedSequence
|
|
194
|
+
&& concurrentIds.has(entry.turn_id)
|
|
195
|
+
))
|
|
196
|
+
.sort((left, right) => left.accepted_sequence - right.accepted_sequence);
|
|
197
|
+
|
|
198
|
+
if (acceptedConcurrentSiblings.length === 0) {
|
|
199
|
+
return observation;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const siblingMarkersByFile = new Map();
|
|
203
|
+
for (const entry of acceptedConcurrentSiblings) {
|
|
204
|
+
const siblingFiles = Array.isArray(entry?.observed_artifact?.files_changed)
|
|
205
|
+
? entry.observed_artifact.files_changed
|
|
206
|
+
: Array.isArray(entry?.files_changed)
|
|
207
|
+
? entry.files_changed
|
|
208
|
+
: [];
|
|
209
|
+
const siblingMarkers = entry?.observed_artifact?.file_markers;
|
|
210
|
+
if (!siblingMarkers || typeof siblingMarkers !== 'object' || Array.isArray(siblingMarkers)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
for (const filePath of siblingFiles) {
|
|
214
|
+
if (typeof siblingMarkers[filePath] === 'string' && siblingMarkers[filePath].length > 0) {
|
|
215
|
+
siblingMarkersByFile.set(filePath, siblingMarkers[filePath]);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (siblingMarkersByFile.size === 0) {
|
|
221
|
+
return observation;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const nextFiles = [];
|
|
225
|
+
const nextMarkers = {};
|
|
226
|
+
const attributedToConcurrentSiblings = [];
|
|
227
|
+
const observationMarkers = observation?.file_markers && typeof observation.file_markers === 'object'
|
|
228
|
+
? observation.file_markers
|
|
229
|
+
: {};
|
|
230
|
+
|
|
231
|
+
for (const filePath of observedFiles) {
|
|
232
|
+
const currentMarker = observationMarkers[filePath];
|
|
233
|
+
const siblingMarker = siblingMarkersByFile.get(filePath);
|
|
234
|
+
if (typeof siblingMarker === 'string' && siblingMarker === currentMarker) {
|
|
235
|
+
attributedToConcurrentSiblings.push(filePath);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
nextFiles.push(filePath);
|
|
239
|
+
if (typeof currentMarker === 'string') {
|
|
240
|
+
nextMarkers[filePath] = currentMarker;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (attributedToConcurrentSiblings.length === 0) {
|
|
245
|
+
return observation;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...observation,
|
|
250
|
+
files_changed: nextFiles,
|
|
251
|
+
file_markers: nextMarkers,
|
|
252
|
+
attributed_to_concurrent_siblings: attributedToConcurrentSiblings,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Build the file set used for acceptance-time conflict detection.
|
|
258
|
+
*
|
|
259
|
+
* Parallel attribution removes unchanged sibling files from the current turn's
|
|
260
|
+
* observed set so declared-vs-observed checks stay truthful. Conflict detection
|
|
261
|
+
* is stricter: if the agent declared a file that still appears in the raw
|
|
262
|
+
* baseline-to-now workspace union, that overlap must remain conflict-eligible
|
|
263
|
+
* even if the current file contents match a sibling's accepted marker.
|
|
264
|
+
*
|
|
265
|
+
* @param {object} rawObservation — unfiltered observeChanges() result
|
|
266
|
+
* @param {object} attributedObservation — result after attributeObservedChangesToTurn()
|
|
267
|
+
* @param {string[]} declaredFiles — turnResult.files_changed
|
|
268
|
+
* @returns {string[]}
|
|
269
|
+
*/
|
|
270
|
+
export function buildConflictCandidateFiles(rawObservation, attributedObservation, declaredFiles = []) {
|
|
271
|
+
const conflictFiles = new Set(
|
|
272
|
+
Array.isArray(attributedObservation?.files_changed) ? attributedObservation.files_changed : [],
|
|
273
|
+
);
|
|
274
|
+
const rawObserved = new Set(
|
|
275
|
+
Array.isArray(rawObservation?.files_changed) ? rawObservation.files_changed : [],
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
for (const filePath of declaredFiles || []) {
|
|
279
|
+
if (rawObserved.has(filePath)) {
|
|
280
|
+
conflictFiles.add(filePath);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return [...conflictFiles].sort();
|
|
285
|
+
}
|
|
286
|
+
|
|
172
287
|
/**
|
|
173
288
|
* Classify observed file changes into added, modified, and deleted.
|
|
174
289
|
*
|
|
@@ -284,13 +399,21 @@ function getFilteredChanges(root, baseRef, filter) {
|
|
|
284
399
|
* @returns {object}
|
|
285
400
|
*/
|
|
286
401
|
export function buildObservedArtifact(observation, baseline) {
|
|
287
|
-
|
|
402
|
+
const artifact = {
|
|
288
403
|
derived_by: 'orchestrator',
|
|
289
404
|
baseline_ref: baseline?.head_ref ? `git:${baseline.head_ref}` : null,
|
|
290
405
|
accepted_ref: observation.head_ref ? `git:${observation.head_ref}` : 'workspace:dirty',
|
|
291
406
|
files_changed: observation.files_changed,
|
|
407
|
+
file_markers: observation.file_markers || {},
|
|
292
408
|
diff_summary: observation.diff_summary,
|
|
293
409
|
};
|
|
410
|
+
// Preserve parallel attribution so operators can see which files were
|
|
411
|
+
// attributed to concurrent siblings and excluded from this turn.
|
|
412
|
+
const attributed = observation.attributed_to_concurrent_siblings;
|
|
413
|
+
if (Array.isArray(attributed) && attributed.length > 0) {
|
|
414
|
+
artifact.attributed_to_concurrent_siblings = attributed;
|
|
415
|
+
}
|
|
416
|
+
return artifact;
|
|
294
417
|
}
|
|
295
418
|
|
|
296
419
|
// ── Verification Normalization ──────────────────────────────────────────────
|
|
@@ -572,6 +695,14 @@ function getWorkspaceFileMarker(root, filePath) {
|
|
|
572
695
|
}
|
|
573
696
|
}
|
|
574
697
|
|
|
698
|
+
function buildFileMarkers(root, filePaths) {
|
|
699
|
+
const markers = {};
|
|
700
|
+
for (const filePath of filePaths || []) {
|
|
701
|
+
markers[filePath] = getWorkspaceFileMarker(root, filePath);
|
|
702
|
+
}
|
|
703
|
+
return markers;
|
|
704
|
+
}
|
|
705
|
+
|
|
575
706
|
function getUntrackedFiles(root) {
|
|
576
707
|
try {
|
|
577
708
|
const result = execSync('git ls-files --others --exclude-standard', {
|