agentxchain 2.99.0 → 2.101.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 +7 -1
- package/package.json +1 -1
- package/scripts/normalize-release-note-sidebar-positions.mjs +81 -0
- package/scripts/release-bump.sh +19 -13
- package/src/commands/diff.js +14 -0
- package/src/commands/verify.js +136 -0
- package/src/lib/export-diff.js +261 -0
- package/src/lib/export.js +7 -0
package/bin/agentxchain.js
CHANGED
|
@@ -59,7 +59,7 @@ import { generateCommand } from '../src/commands/generate.js';
|
|
|
59
59
|
import { doctorCommand } from '../src/commands/doctor.js';
|
|
60
60
|
import { superviseCommand } from '../src/commands/supervise.js';
|
|
61
61
|
import { validateCommand } from '../src/commands/validate.js';
|
|
62
|
-
import { verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
|
|
62
|
+
import { verifyDiffCommand, verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
|
|
63
63
|
import { replayTurnCommand } from '../src/commands/replay.js';
|
|
64
64
|
import { replayExportCommand } from '../src/commands/replay-export.js';
|
|
65
65
|
import { kickoffCommand } from '../src/commands/kickoff.js';
|
|
@@ -401,6 +401,12 @@ verifyCmd
|
|
|
401
401
|
.option('--format <format>', 'Output format: text or json', 'text')
|
|
402
402
|
.action(verifyExportCommand);
|
|
403
403
|
|
|
404
|
+
verifyCmd
|
|
405
|
+
.command('diff <left_export> <right_export>')
|
|
406
|
+
.description('Verify two export artifacts, then detect governance regressions between them')
|
|
407
|
+
.option('--format <format>', 'Output format: text or json', 'text')
|
|
408
|
+
.action(verifyDiffCommand);
|
|
409
|
+
|
|
404
410
|
const replayCmd = program
|
|
405
411
|
.command('replay')
|
|
406
412
|
.description('Replay accepted governed evidence against the current workspace');
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const repoRoot = resolve(__dirname, '..', '..');
|
|
9
|
+
const releasesDir = join(repoRoot, 'website-v2', 'docs', 'releases');
|
|
10
|
+
|
|
11
|
+
function parseReleaseFile(file) {
|
|
12
|
+
const match = /^v(\d+)-(\d+)-(\d+)\.mdx$/.exec(file);
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new Error(`Unparseable release note filename: ${file}`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
file,
|
|
18
|
+
version: match.slice(1).map(Number),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compareDesc(left, right) {
|
|
23
|
+
for (let i = 0; i < 3; i += 1) {
|
|
24
|
+
if (left.version[i] !== right.version[i]) {
|
|
25
|
+
return right.version[i] - left.version[i];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateSidebarPosition(content, nextPosition) {
|
|
32
|
+
if (!content.startsWith('---\n')) {
|
|
33
|
+
throw new Error('Release note is missing YAML frontmatter opening delimiter');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const frontmatterEnd = content.indexOf('\n---\n', 4);
|
|
37
|
+
if (frontmatterEnd === -1) {
|
|
38
|
+
throw new Error('Release note is missing YAML frontmatter closing delimiter');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const frontmatter = content.slice(0, frontmatterEnd + 5);
|
|
42
|
+
const body = content.slice(frontmatterEnd + 5);
|
|
43
|
+
const expectedLine = `sidebar_position: ${nextPosition}`;
|
|
44
|
+
|
|
45
|
+
let updatedFrontmatter;
|
|
46
|
+
if (/^sidebar_position:\s*\d+\s*$/m.test(frontmatter)) {
|
|
47
|
+
updatedFrontmatter = frontmatter.replace(/^sidebar_position:\s*\d+\s*$/m, expectedLine);
|
|
48
|
+
} else {
|
|
49
|
+
updatedFrontmatter = frontmatter.replace(/^---\n/, `---\n${expectedLine}\n`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const updated = updatedFrontmatter + body;
|
|
53
|
+
return {
|
|
54
|
+
updated,
|
|
55
|
+
changed: updated !== content,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const releaseFiles = readdirSync(releasesDir)
|
|
60
|
+
.filter((file) => /^v\d+-\d+-\d+\.mdx$/.test(file))
|
|
61
|
+
.map(parseReleaseFile)
|
|
62
|
+
.sort(compareDesc);
|
|
63
|
+
|
|
64
|
+
if (releaseFiles.length === 0) {
|
|
65
|
+
throw new Error(`No release notes found in ${releasesDir}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let changedCount = 0;
|
|
69
|
+
for (const [index, release] of releaseFiles.entries()) {
|
|
70
|
+
const filePath = join(releasesDir, release.file);
|
|
71
|
+
const content = readFileSync(filePath, 'utf8');
|
|
72
|
+
const { updated, changed } = updateSidebarPosition(content, index);
|
|
73
|
+
if (changed) {
|
|
74
|
+
writeFileSync(filePath, updated);
|
|
75
|
+
changedCount += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(
|
|
80
|
+
`Normalized release note sidebar positions across ${releaseFiles.length} files (${changedCount} updated).`,
|
|
81
|
+
);
|
package/scripts/release-bump.sh
CHANGED
|
@@ -192,13 +192,18 @@ if [[ "${#SURFACE_ERRORS[@]}" -gt 0 ]]; then
|
|
|
192
192
|
fi
|
|
193
193
|
echo " OK: all 8 governed version surfaces reference ${TARGET_VERSION}"
|
|
194
194
|
|
|
195
|
-
# 5.
|
|
195
|
+
# 5. Normalize release-note sidebar ordering
|
|
196
|
+
echo "[5/10] Normalizing release-note sidebar positions..."
|
|
197
|
+
node "${CLI_DIR}/scripts/normalize-release-note-sidebar-positions.mjs"
|
|
198
|
+
echo " OK: release-note sidebar positions normalized newest-first"
|
|
199
|
+
|
|
200
|
+
# 6. Auto-align Homebrew mirror to target version
|
|
196
201
|
# The formula URL and README version/tarball are updated automatically.
|
|
197
202
|
# The SHA256 is carried from the previous committed formula — it is inherently a
|
|
198
203
|
# post-publish artifact (npm registry tarballs are not byte-identical to
|
|
199
204
|
# local npm-pack output). Any working-tree SHA edit is overwritten here.
|
|
200
205
|
# sync-homebrew.sh corrects the SHA after publish.
|
|
201
|
-
echo "[
|
|
206
|
+
echo "[6/10] Auto-aligning Homebrew mirror to ${TARGET_VERSION}..."
|
|
202
207
|
HOMEBREW_MIRROR="${REPO_ROOT}/cli/homebrew/agentxchain.rb"
|
|
203
208
|
HOMEBREW_MIRROR_README="${REPO_ROOT}/cli/homebrew/README.md"
|
|
204
209
|
TARBALL_URL="https://registry.npmjs.org/agentxchain/-/agentxchain-${TARGET_VERSION}.tgz"
|
|
@@ -256,13 +261,13 @@ else
|
|
|
256
261
|
echo " Skipped: no Homebrew mirror files found"
|
|
257
262
|
fi
|
|
258
263
|
|
|
259
|
-
#
|
|
260
|
-
echo "[
|
|
264
|
+
# 7. Update version files (no git operations)
|
|
265
|
+
echo "[7/10] Updating version files..."
|
|
261
266
|
npm version "$TARGET_VERSION" --no-git-tag-version
|
|
262
267
|
echo " OK: package.json updated to ${TARGET_VERSION}"
|
|
263
268
|
|
|
264
|
-
#
|
|
265
|
-
echo "[
|
|
269
|
+
# 8. Stage version files
|
|
270
|
+
echo "[8/10] Staging version files..."
|
|
266
271
|
git add -- package.json
|
|
267
272
|
if [[ -f package-lock.json ]]; then
|
|
268
273
|
git add -- package-lock.json
|
|
@@ -270,10 +275,11 @@ fi
|
|
|
270
275
|
for rel_path in "${ALLOWED_RELEASE_PATHS[@]}"; do
|
|
271
276
|
stage_if_present "$rel_path"
|
|
272
277
|
done
|
|
278
|
+
git -C "$REPO_ROOT" add -- website-v2/docs/releases
|
|
273
279
|
echo " OK: version files and allowed release surfaces staged"
|
|
274
280
|
|
|
275
|
-
#
|
|
276
|
-
echo "[
|
|
281
|
+
# 9. Create release commit
|
|
282
|
+
echo "[9/10] Creating release commit..."
|
|
277
283
|
git commit -m "${TARGET_VERSION}"
|
|
278
284
|
RELEASE_SHA=$(git rev-parse HEAD)
|
|
279
285
|
COMMIT_MSG=$(git log -1 --format=%s)
|
|
@@ -283,13 +289,13 @@ if [[ "$COMMIT_MSG" != "$TARGET_VERSION" ]]; then
|
|
|
283
289
|
fi
|
|
284
290
|
echo " OK: commit ${RELEASE_SHA:0:7} with message '${TARGET_VERSION}'"
|
|
285
291
|
|
|
286
|
-
#
|
|
292
|
+
# 9.5. Inline preflight gate — tests, pack, and docs build must pass before tag
|
|
287
293
|
if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
|
|
288
294
|
echo ""
|
|
289
|
-
echo "[
|
|
295
|
+
echo "[9.5/11] Inline preflight gate SKIPPED (--skip-preflight)"
|
|
290
296
|
else
|
|
291
297
|
echo ""
|
|
292
|
-
echo "[
|
|
298
|
+
echo "[9.5/11] Running inline preflight gate..."
|
|
293
299
|
echo " Running test suite..."
|
|
294
300
|
|
|
295
301
|
# Install MCP example deps if needed (same as release-preflight.sh)
|
|
@@ -342,8 +348,8 @@ else
|
|
|
342
348
|
echo " Inline preflight gate passed — proceeding to tag"
|
|
343
349
|
fi
|
|
344
350
|
|
|
345
|
-
#
|
|
346
|
-
echo "[
|
|
351
|
+
# 10. Create annotated tag
|
|
352
|
+
echo "[10/11] Creating annotated tag..."
|
|
347
353
|
git tag -a "v${TARGET_VERSION}" -m "v${TARGET_VERSION}"
|
|
348
354
|
TAG_SHA=$(git rev-parse "v${TARGET_VERSION}")
|
|
349
355
|
if [[ -z "$TAG_SHA" ]]; then
|
package/src/commands/diff.js
CHANGED
|
@@ -132,6 +132,8 @@ function formatExportDiffText(diff) {
|
|
|
132
132
|
.map((entry) => `${entry.key}: ${formatValue(entry.left)} -> ${formatValue(entry.right)}${formatDelta(entry.delta, entry.label)}`));
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
appendRegressionSection(lines, diff.regressions);
|
|
136
|
+
|
|
135
137
|
return lines.join('\n');
|
|
136
138
|
}
|
|
137
139
|
|
|
@@ -144,6 +146,18 @@ function appendChangedSection(lines, heading, items) {
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
function appendRegressionSection(lines, regressions) {
|
|
150
|
+
if (!regressions || regressions.length === 0) return;
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push(chalk.bold.red('Governance Regressions:'));
|
|
153
|
+
for (const reg of regressions) {
|
|
154
|
+
const severityTag = reg.severity === 'error'
|
|
155
|
+
? chalk.red(`[${reg.severity}]`)
|
|
156
|
+
: chalk.yellow(`[${reg.severity}]`);
|
|
157
|
+
lines.push(`${severityTag} ${reg.id}: ${reg.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
function listChangeItems(entry) {
|
|
148
162
|
const items = [];
|
|
149
163
|
if (entry.added.length > 0) {
|
package/src/commands/verify.js
CHANGED
|
@@ -6,6 +6,7 @@ import { validateStagedTurnResult } from '../lib/turn-result-validator.js';
|
|
|
6
6
|
import { getTurnStagingResultPath } from '../lib/turn-paths.js';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import { loadExportArtifact, verifyExportArtifact } from '../lib/export-verifier.js';
|
|
9
|
+
import { buildExportDiff, resolveExportArtifact } from '../lib/export-diff.js';
|
|
9
10
|
import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
|
|
10
11
|
import {
|
|
11
12
|
DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
|
|
@@ -72,6 +73,18 @@ function emitProtocolVerifyError(format, message) {
|
|
|
72
73
|
console.log(chalk.red(`Protocol verification failed: ${message}`));
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function emitVerifyDiffError(format, message) {
|
|
77
|
+
if (format === 'json') {
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
overall: 'error',
|
|
80
|
+
message,
|
|
81
|
+
}, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(chalk.red(`Diff verification failed: ${message}`));
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
export async function verifyExportCommand(opts) {
|
|
76
89
|
const format = opts.format || 'text';
|
|
77
90
|
const loaded = loadExportArtifact(opts.input || '-', process.cwd());
|
|
@@ -104,6 +117,75 @@ export async function verifyExportCommand(opts) {
|
|
|
104
117
|
process.exit(result.ok ? 0 : 1);
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
export async function verifyDiffCommand(leftRef, rightRef, opts = {}) {
|
|
121
|
+
const format = opts.format || 'text';
|
|
122
|
+
const leftLoaded = resolveExportArtifact(leftRef);
|
|
123
|
+
if (!leftLoaded.ok) {
|
|
124
|
+
emitVerifyDiffError(format, leftLoaded.error);
|
|
125
|
+
process.exit(2);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const rightLoaded = resolveExportArtifact(rightRef);
|
|
129
|
+
if (!rightLoaded.ok) {
|
|
130
|
+
emitVerifyDiffError(format, rightLoaded.error);
|
|
131
|
+
process.exit(2);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (leftLoaded.artifact.export_kind !== rightLoaded.artifact.export_kind) {
|
|
135
|
+
emitVerifyDiffError(
|
|
136
|
+
format,
|
|
137
|
+
`Export kinds do not match: ${leftLoaded.artifact.export_kind} vs ${rightLoaded.artifact.export_kind}`,
|
|
138
|
+
);
|
|
139
|
+
process.exit(2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const leftVerification = verifyExportArtifact(leftLoaded.artifact);
|
|
143
|
+
const rightVerification = verifyExportArtifact(rightLoaded.artifact);
|
|
144
|
+
const leftReport = { ...leftVerification.report, input: leftLoaded.resolved_ref };
|
|
145
|
+
const rightReport = { ...rightVerification.report, input: rightLoaded.resolved_ref };
|
|
146
|
+
|
|
147
|
+
let diff = null;
|
|
148
|
+
let overall = 'pass';
|
|
149
|
+
let exitCode = 0;
|
|
150
|
+
let message = null;
|
|
151
|
+
|
|
152
|
+
if (!leftVerification.ok || !rightVerification.ok) {
|
|
153
|
+
overall = 'fail';
|
|
154
|
+
exitCode = 1;
|
|
155
|
+
message = 'Diff skipped because one or both exports failed verification.';
|
|
156
|
+
} else {
|
|
157
|
+
const diffResult = buildExportDiff(leftLoaded.artifact, rightLoaded.artifact, {
|
|
158
|
+
left_ref: leftLoaded.resolved_ref,
|
|
159
|
+
right_ref: rightLoaded.resolved_ref,
|
|
160
|
+
});
|
|
161
|
+
if (!diffResult.ok) {
|
|
162
|
+
emitVerifyDiffError(format, diffResult.error);
|
|
163
|
+
process.exit(2);
|
|
164
|
+
}
|
|
165
|
+
diff = diffResult.diff;
|
|
166
|
+
if (diff.has_regressions) {
|
|
167
|
+
overall = 'fail';
|
|
168
|
+
exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const report = {
|
|
173
|
+
overall,
|
|
174
|
+
left: leftReport,
|
|
175
|
+
right: rightReport,
|
|
176
|
+
diff,
|
|
177
|
+
message,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (format === 'json') {
|
|
181
|
+
console.log(JSON.stringify(report, null, 2));
|
|
182
|
+
} else {
|
|
183
|
+
printVerifyDiffReport(report);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
process.exit(exitCode);
|
|
187
|
+
}
|
|
188
|
+
|
|
107
189
|
export async function verifyTurnCommand(turnId, opts = {}) {
|
|
108
190
|
const context = loadProjectContext();
|
|
109
191
|
if (!context) {
|
|
@@ -237,6 +319,54 @@ function printExportReport(report) {
|
|
|
237
319
|
console.log('');
|
|
238
320
|
}
|
|
239
321
|
|
|
322
|
+
function printVerifyDiffReport(report) {
|
|
323
|
+
console.log('');
|
|
324
|
+
console.log(chalk.bold(' AgentXchain Diff Verification'));
|
|
325
|
+
console.log(chalk.dim(' ' + '─'.repeat(41)));
|
|
326
|
+
console.log(chalk.dim(` Left: ${report.left.input}`));
|
|
327
|
+
console.log(chalk.dim(` Right: ${report.right.input}`));
|
|
328
|
+
console.log('');
|
|
329
|
+
|
|
330
|
+
const overallLabel = report.overall === 'pass'
|
|
331
|
+
? chalk.green('PASS')
|
|
332
|
+
: report.overall === 'fail'
|
|
333
|
+
? chalk.red('FAIL')
|
|
334
|
+
: chalk.red('ERROR');
|
|
335
|
+
console.log(` Overall: ${overallLabel}`);
|
|
336
|
+
console.log(` Left export: ${formatVerifyStatus(report.left.overall)}`);
|
|
337
|
+
console.log(` Right export: ${formatVerifyStatus(report.right.overall)}`);
|
|
338
|
+
|
|
339
|
+
if (report.diff) {
|
|
340
|
+
console.log(chalk.dim(` Diff subject: ${report.diff.subject_kind}`));
|
|
341
|
+
console.log(chalk.dim(` Changed: ${report.diff.changed ? 'yes' : 'no'}`));
|
|
342
|
+
console.log(chalk.dim(` Governance regressions: ${report.diff.regression_count}`));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (report.message) {
|
|
346
|
+
console.log(chalk.yellow(` ${report.message}`));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const error of report.left.errors || []) {
|
|
350
|
+
console.log(chalk.red(` left ✗ ${error}`));
|
|
351
|
+
}
|
|
352
|
+
for (const error of report.right.errors || []) {
|
|
353
|
+
console.log(chalk.red(` right ✗ ${error}`));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (report.diff?.regressions?.length > 0) {
|
|
357
|
+
console.log('');
|
|
358
|
+
console.log(chalk.bold.red(' Governance Regressions'));
|
|
359
|
+
for (const regression of report.diff.regressions) {
|
|
360
|
+
const severity = regression.severity === 'error'
|
|
361
|
+
? chalk.red(`[${regression.severity}]`)
|
|
362
|
+
: chalk.yellow(`[${regression.severity}]`);
|
|
363
|
+
console.log(` ${severity} ${regression.id}: ${regression.message}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log('');
|
|
368
|
+
}
|
|
369
|
+
|
|
240
370
|
function resolveTargetTurnId(requestedTurnId, activeTurns) {
|
|
241
371
|
const turnIds = Object.keys(activeTurns);
|
|
242
372
|
|
|
@@ -351,3 +481,9 @@ function formatOutcome(outcome) {
|
|
|
351
481
|
if (outcome === 'mismatch') return chalk.red('mismatch');
|
|
352
482
|
return chalk.yellow('not_reproducible');
|
|
353
483
|
}
|
|
484
|
+
|
|
485
|
+
function formatVerifyStatus(status) {
|
|
486
|
+
if (status === 'pass') return chalk.green('PASS');
|
|
487
|
+
if (status === 'fail') return chalk.red('FAIL');
|
|
488
|
+
return chalk.red(String(status || 'error').toUpperCase());
|
|
489
|
+
}
|
package/src/lib/export-diff.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
|
|
4
|
+
const FAILED_STATUSES = new Set(['failed', 'error', 'crashed']);
|
|
5
|
+
|
|
4
6
|
const RUN_EXPORT_SCALAR_FIELDS = [
|
|
5
7
|
['run_id', 'Run ID'],
|
|
6
8
|
['status', 'Status'],
|
|
@@ -119,6 +121,7 @@ function buildRunExportDiff(leftArtifact, rightArtifact, refs) {
|
|
|
119
121
|
};
|
|
120
122
|
|
|
121
123
|
const changed = hasChanged(scalar_changes, numeric_changes, list_changes);
|
|
124
|
+
const regressions = detectRunRegressions(left, right);
|
|
122
125
|
|
|
123
126
|
return {
|
|
124
127
|
comparison_mode: 'export',
|
|
@@ -132,6 +135,9 @@ function buildRunExportDiff(leftArtifact, rightArtifact, refs) {
|
|
|
132
135
|
scalar_changes,
|
|
133
136
|
numeric_changes,
|
|
134
137
|
list_changes,
|
|
138
|
+
regressions,
|
|
139
|
+
regression_count: regressions.length,
|
|
140
|
+
has_regressions: regressions.length > 0,
|
|
135
141
|
};
|
|
136
142
|
}
|
|
137
143
|
|
|
@@ -153,6 +159,7 @@ function buildCoordinatorExportDiff(leftArtifact, rightArtifact, refs) {
|
|
|
153
159
|
|| repo_status_changes.some((entry) => entry.changed)
|
|
154
160
|
|| repo_export_changes.some((entry) => entry.changed)
|
|
155
161
|
|| event_type_changes.some((entry) => entry.changed);
|
|
162
|
+
const regressions = detectCoordinatorRegressions(left, right);
|
|
156
163
|
|
|
157
164
|
return {
|
|
158
165
|
comparison_mode: 'export',
|
|
@@ -169,17 +176,24 @@ function buildCoordinatorExportDiff(leftArtifact, rightArtifact, refs) {
|
|
|
169
176
|
repo_status_changes,
|
|
170
177
|
repo_export_changes,
|
|
171
178
|
event_type_changes,
|
|
179
|
+
regressions,
|
|
180
|
+
regression_count: regressions.length,
|
|
181
|
+
has_regressions: regressions.length > 0,
|
|
172
182
|
};
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
function normalizeRunExport(artifact) {
|
|
176
186
|
const summary = artifact.summary || {};
|
|
177
187
|
const repoDecisions = summary.repo_decisions || {};
|
|
188
|
+
const state = artifact.state || {};
|
|
189
|
+
const budgetStatus = state.budget_status || {};
|
|
190
|
+
const phaseGateStatus = state.phase_gate_status || {};
|
|
178
191
|
return {
|
|
179
192
|
export_kind: artifact.export_kind,
|
|
180
193
|
run_id: summary.run_id || null,
|
|
181
194
|
status: summary.status || null,
|
|
182
195
|
phase: summary.phase || null,
|
|
196
|
+
workflow_phase_order: Array.isArray(summary.workflow_phase_order) ? summary.workflow_phase_order : null,
|
|
183
197
|
project_name: artifact.project?.name || null,
|
|
184
198
|
project_goal: summary.project_goal || artifact.project?.goal || null,
|
|
185
199
|
provenance_trigger: summary.provenance?.trigger || null,
|
|
@@ -197,6 +211,10 @@ function normalizeRunExport(artifact) {
|
|
|
197
211
|
retained_turn_ids: normalizeStringArray(summary.retained_turn_ids),
|
|
198
212
|
active_repo_decision_ids: normalizeStringArray((repoDecisions.active || []).map((entry) => entry?.id)),
|
|
199
213
|
overridden_repo_decision_ids: normalizeStringArray((repoDecisions.overridden || []).map((entry) => entry?.id)),
|
|
214
|
+
budget_warn_mode: budgetStatus.warn_mode === true,
|
|
215
|
+
budget_exhausted: budgetStatus.exhausted === true,
|
|
216
|
+
phase_gate_status: normalizeGateStatusMap(phaseGateStatus),
|
|
217
|
+
delegation_missing_decisions: normalizeDelegationMissingMap(summary.delegation_summary),
|
|
200
218
|
};
|
|
201
219
|
}
|
|
202
220
|
|
|
@@ -211,6 +229,7 @@ function normalizeCoordinatorExport(artifact) {
|
|
|
211
229
|
super_run_id: summary.super_run_id || null,
|
|
212
230
|
status: summary.status || null,
|
|
213
231
|
phase: summary.phase || null,
|
|
232
|
+
workflow_phase_order: Array.isArray(summary.workflow_phase_order) ? summary.workflow_phase_order : null,
|
|
214
233
|
project_name: artifact.coordinator?.project_name || null,
|
|
215
234
|
barrier_count: toNumber(summary.barrier_count),
|
|
216
235
|
history_entries: toNumber(summary.history_entries),
|
|
@@ -338,6 +357,27 @@ function normalizeNumericMap(value) {
|
|
|
338
357
|
);
|
|
339
358
|
}
|
|
340
359
|
|
|
360
|
+
function normalizeDelegationMissingMap(summary) {
|
|
361
|
+
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) return {};
|
|
362
|
+
const chains = Array.isArray(summary.delegation_chains) ? summary.delegation_chains : [];
|
|
363
|
+
const entries = [];
|
|
364
|
+
for (const chain of chains) {
|
|
365
|
+
const delegations = Array.isArray(chain?.delegations) ? chain.delegations : [];
|
|
366
|
+
for (const delegation of delegations) {
|
|
367
|
+
if (!delegation || typeof delegation !== 'object' || Array.isArray(delegation)) continue;
|
|
368
|
+
const delegationId = typeof delegation.delegation_id === 'string' && delegation.delegation_id.trim()
|
|
369
|
+
? delegation.delegation_id.trim()
|
|
370
|
+
: null;
|
|
371
|
+
if (!delegationId) continue;
|
|
372
|
+
entries.push([
|
|
373
|
+
delegationId,
|
|
374
|
+
normalizeStringArray(delegation.missing_decision_ids),
|
|
375
|
+
]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right, 'en')));
|
|
379
|
+
}
|
|
380
|
+
|
|
341
381
|
function toNumber(value) {
|
|
342
382
|
return typeof value === 'number' ? value : null;
|
|
343
383
|
}
|
|
@@ -345,3 +385,224 @@ function toNumber(value) {
|
|
|
345
385
|
function isEqual(left, right) {
|
|
346
386
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
347
387
|
}
|
|
388
|
+
|
|
389
|
+
function normalizeGateStatusMap(gateStatus) {
|
|
390
|
+
if (!gateStatus || typeof gateStatus !== 'object' || Array.isArray(gateStatus)) return {};
|
|
391
|
+
return Object.fromEntries(
|
|
392
|
+
Object.entries(gateStatus)
|
|
393
|
+
.filter(([key]) => typeof key === 'string' && key.trim().length > 0)
|
|
394
|
+
.map(([key, value]) => [key.trim(), typeof value === 'string' ? value.trim() : String(value)]),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const GATE_PASSED_STATES = new Set(['passed', 'approved', 'satisfied']);
|
|
399
|
+
const GATE_FAILED_STATES = new Set(['failed', 'blocked', 'rejected']);
|
|
400
|
+
|
|
401
|
+
function detectRunRegressions(left, right) {
|
|
402
|
+
const regressions = [];
|
|
403
|
+
let counter = 0;
|
|
404
|
+
|
|
405
|
+
// Status regression: completed/active -> failed/error/crashed
|
|
406
|
+
if (left.status && right.status && !FAILED_STATUSES.has(left.status) && FAILED_STATUSES.has(right.status)) {
|
|
407
|
+
regressions.push({
|
|
408
|
+
id: `REG-STATUS-${String(++counter).padStart(3, '0')}`,
|
|
409
|
+
category: 'status',
|
|
410
|
+
severity: 'error',
|
|
411
|
+
message: `Run status regressed from ${left.status} to ${right.status}`,
|
|
412
|
+
field: 'status',
|
|
413
|
+
left: left.status,
|
|
414
|
+
right: right.status,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Phase regression: backward movement in workflow phase order
|
|
419
|
+
if (left.phase && right.phase === null) {
|
|
420
|
+
// Phase disappeared — information loss
|
|
421
|
+
regressions.push({
|
|
422
|
+
id: `REG-PHASE-${String(++counter).padStart(3, '0')}`,
|
|
423
|
+
category: 'phase',
|
|
424
|
+
severity: 'warning',
|
|
425
|
+
message: `Phase regressed from "${left.phase}" to null (phase information lost)`,
|
|
426
|
+
field: 'phase',
|
|
427
|
+
left: left.phase,
|
|
428
|
+
right: null,
|
|
429
|
+
});
|
|
430
|
+
} else if (left.phase && right.phase && left.phase !== right.phase) {
|
|
431
|
+
// Use right export's phase order as canonical (or left if right doesn't have one)
|
|
432
|
+
const phaseOrder = right.workflow_phase_order || left.workflow_phase_order;
|
|
433
|
+
if (Array.isArray(phaseOrder) && phaseOrder.length > 0) {
|
|
434
|
+
const leftIndex = phaseOrder.indexOf(left.phase);
|
|
435
|
+
const rightIndex = phaseOrder.indexOf(right.phase);
|
|
436
|
+
// Only flag when both phases are known and right is earlier than left
|
|
437
|
+
if (leftIndex !== -1 && rightIndex !== -1 && rightIndex < leftIndex) {
|
|
438
|
+
regressions.push({
|
|
439
|
+
id: `REG-PHASE-${String(++counter).padStart(3, '0')}`,
|
|
440
|
+
category: 'phase',
|
|
441
|
+
severity: 'warning',
|
|
442
|
+
message: `Phase moved backward from "${left.phase}" (position ${leftIndex}) to "${right.phase}" (position ${rightIndex})`,
|
|
443
|
+
field: 'phase',
|
|
444
|
+
left: left.phase,
|
|
445
|
+
right: right.phase,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Budget warn_mode regression
|
|
452
|
+
if (left.budget_warn_mode === false && right.budget_warn_mode === true) {
|
|
453
|
+
regressions.push({
|
|
454
|
+
id: `REG-BUDGET-WARN-${String(++counter).padStart(3, '0')}`,
|
|
455
|
+
category: 'budget',
|
|
456
|
+
severity: 'warning',
|
|
457
|
+
message: 'Budget entered warn mode (over budget)',
|
|
458
|
+
field: 'budget_warn_mode',
|
|
459
|
+
left: false,
|
|
460
|
+
right: true,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Budget exhausted regression
|
|
465
|
+
if (left.budget_exhausted === false && right.budget_exhausted === true) {
|
|
466
|
+
regressions.push({
|
|
467
|
+
id: `REG-BUDGET-EXHAUST-${String(++counter).padStart(3, '0')}`,
|
|
468
|
+
category: 'budget',
|
|
469
|
+
severity: 'error',
|
|
470
|
+
message: 'Budget exhausted',
|
|
471
|
+
field: 'budget_exhausted',
|
|
472
|
+
left: false,
|
|
473
|
+
right: true,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Decision override count increase
|
|
478
|
+
const leftOverrides = typeof left.overridden_repo_decision_count === 'number' ? left.overridden_repo_decision_count : 0;
|
|
479
|
+
const rightOverrides = typeof right.overridden_repo_decision_count === 'number' ? right.overridden_repo_decision_count : 0;
|
|
480
|
+
if (rightOverrides > leftOverrides) {
|
|
481
|
+
regressions.push({
|
|
482
|
+
id: `REG-DECISION-OVERRIDE-${String(++counter).padStart(3, '0')}`,
|
|
483
|
+
category: 'decisions',
|
|
484
|
+
severity: 'warning',
|
|
485
|
+
message: `Repo decision overrides increased from ${leftOverrides} to ${rightOverrides}`,
|
|
486
|
+
field: 'overridden_repo_decision_count',
|
|
487
|
+
left: leftOverrides,
|
|
488
|
+
right: rightOverrides,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Delegation contract regressions: newly missing required decisions.
|
|
493
|
+
const allDelegationIds = new Set([
|
|
494
|
+
...Object.keys(left.delegation_missing_decisions || {}),
|
|
495
|
+
...Object.keys(right.delegation_missing_decisions || {}),
|
|
496
|
+
]);
|
|
497
|
+
for (const delegationId of allDelegationIds) {
|
|
498
|
+
const leftMissing = normalizeStringArray((left.delegation_missing_decisions || {})[delegationId]);
|
|
499
|
+
const rightMissing = normalizeStringArray((right.delegation_missing_decisions || {})[delegationId]);
|
|
500
|
+
const leftSet = new Set(leftMissing);
|
|
501
|
+
const newlyMissing = rightMissing.filter((decisionId) => !leftSet.has(decisionId));
|
|
502
|
+
if (newlyMissing.length > 0) {
|
|
503
|
+
regressions.push({
|
|
504
|
+
id: `REG-DELEGATION-MISSING-${String(++counter).padStart(3, '0')}`,
|
|
505
|
+
category: 'delegation',
|
|
506
|
+
severity: 'error',
|
|
507
|
+
message: `Delegation "${delegationId}" is now missing required decisions: ${newlyMissing.join(', ')}`,
|
|
508
|
+
field: `delegation_summary.${delegationId}.missing_decision_ids`,
|
|
509
|
+
left: leftMissing,
|
|
510
|
+
right: rightMissing,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Gate regressions: passed/approved -> failed/blocked
|
|
516
|
+
const allGateIds = new Set([...Object.keys(left.phase_gate_status || {}), ...Object.keys(right.phase_gate_status || {})]);
|
|
517
|
+
for (const gateId of allGateIds) {
|
|
518
|
+
const leftGate = (left.phase_gate_status || {})[gateId] || null;
|
|
519
|
+
const rightGate = (right.phase_gate_status || {})[gateId] || null;
|
|
520
|
+
if (leftGate && rightGate && GATE_PASSED_STATES.has(leftGate) && GATE_FAILED_STATES.has(rightGate)) {
|
|
521
|
+
regressions.push({
|
|
522
|
+
id: `REG-GATE-${String(++counter).padStart(3, '0')}`,
|
|
523
|
+
category: 'gate',
|
|
524
|
+
severity: 'error',
|
|
525
|
+
message: `Gate "${gateId}" regressed from ${leftGate} to ${rightGate}`,
|
|
526
|
+
field: `phase_gate_status.${gateId}`,
|
|
527
|
+
left: leftGate,
|
|
528
|
+
right: rightGate,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return regressions;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function detectCoordinatorRegressions(left, right) {
|
|
537
|
+
// Start with the run-level regressions that apply to coordinator summaries
|
|
538
|
+
const regressions = detectRunRegressions(left, right);
|
|
539
|
+
let counter = regressions.length;
|
|
540
|
+
|
|
541
|
+
// Repo status regressions: child repo completed -> failed
|
|
542
|
+
const allRepoIds = new Set([...Object.keys(left.repo_run_statuses || {}), ...Object.keys(right.repo_run_statuses || {})]);
|
|
543
|
+
for (const repoId of allRepoIds) {
|
|
544
|
+
const leftStatus = (left.repo_run_statuses || {})[repoId] || null;
|
|
545
|
+
const rightStatus = (right.repo_run_statuses || {})[repoId] || null;
|
|
546
|
+
if (leftStatus && rightStatus && !FAILED_STATUSES.has(leftStatus) && FAILED_STATUSES.has(rightStatus)) {
|
|
547
|
+
regressions.push({
|
|
548
|
+
id: `REG-REPO-STATUS-${String(++counter).padStart(3, '0')}`,
|
|
549
|
+
category: 'repo_status',
|
|
550
|
+
severity: 'error',
|
|
551
|
+
message: `Child repo "${repoId}" status regressed from ${leftStatus} to ${rightStatus}`,
|
|
552
|
+
field: `repo_run_statuses.${repoId}`,
|
|
553
|
+
left: leftStatus,
|
|
554
|
+
right: rightStatus,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Repo export regressions: ok true -> false
|
|
560
|
+
const allExportRepoIds = new Set([...Object.keys(left.repo_export_status || {}), ...Object.keys(right.repo_export_status || {})]);
|
|
561
|
+
for (const repoId of allExportRepoIds) {
|
|
562
|
+
const leftOk = (left.repo_export_status || {})[repoId];
|
|
563
|
+
const rightOk = (right.repo_export_status || {})[repoId];
|
|
564
|
+
if (leftOk === true && rightOk === false) {
|
|
565
|
+
regressions.push({
|
|
566
|
+
id: `REG-REPO-EXPORT-${String(++counter).padStart(3, '0')}`,
|
|
567
|
+
category: 'repo_export',
|
|
568
|
+
severity: 'error',
|
|
569
|
+
message: `Child repo "${repoId}" export regressed from ok to failed`,
|
|
570
|
+
field: `repo_export_status.${repoId}`,
|
|
571
|
+
left: true,
|
|
572
|
+
right: false,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Barrier count decrease
|
|
578
|
+
const leftBarriers = typeof left.barrier_count === 'number' ? left.barrier_count : 0;
|
|
579
|
+
const rightBarriers = typeof right.barrier_count === 'number' ? right.barrier_count : 0;
|
|
580
|
+
if (leftBarriers > 0 && rightBarriers < leftBarriers) {
|
|
581
|
+
regressions.push({
|
|
582
|
+
id: `REG-BARRIER-${String(++counter).padStart(3, '0')}`,
|
|
583
|
+
category: 'barrier',
|
|
584
|
+
severity: 'warning',
|
|
585
|
+
message: `Barrier count decreased from ${leftBarriers} to ${rightBarriers}`,
|
|
586
|
+
field: 'barrier_count',
|
|
587
|
+
left: leftBarriers,
|
|
588
|
+
right: rightBarriers,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Event loss: total_events decreased
|
|
593
|
+
const leftEvents = typeof left.total_events === 'number' ? left.total_events : 0;
|
|
594
|
+
const rightEvents = typeof right.total_events === 'number' ? right.total_events : 0;
|
|
595
|
+
if (leftEvents > 0 && rightEvents < leftEvents) {
|
|
596
|
+
regressions.push({
|
|
597
|
+
id: `REG-EVENT-LOSS-${String(++counter).padStart(3, '0')}`,
|
|
598
|
+
category: 'events',
|
|
599
|
+
severity: 'warning',
|
|
600
|
+
message: `Aggregated event count decreased from ${leftEvents} to ${rightEvents}`,
|
|
601
|
+
field: 'total_events',
|
|
602
|
+
left: leftEvents,
|
|
603
|
+
right: rightEvents,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return regressions;
|
|
608
|
+
}
|
package/src/lib/export.js
CHANGED
|
@@ -454,6 +454,9 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
454
454
|
run_id: state?.run_id || null,
|
|
455
455
|
status: state?.status || null,
|
|
456
456
|
phase: state?.phase || null,
|
|
457
|
+
workflow_phase_order: config.routing && Object.keys(config.routing).length > 0
|
|
458
|
+
? Object.keys(config.routing)
|
|
459
|
+
: null,
|
|
457
460
|
provenance: normalizeRunProvenance(state?.provenance),
|
|
458
461
|
inherited_context: state?.inherited_context || null,
|
|
459
462
|
active_turn_ids: activeTurns,
|
|
@@ -640,6 +643,10 @@ export function buildCoordinatorExport(startDir = process.cwd()) {
|
|
|
640
643
|
super_run_id: coordState?.super_run_id || null,
|
|
641
644
|
status: coordState?.status || null,
|
|
642
645
|
phase: coordState?.phase || null,
|
|
646
|
+
workflow_phase_order: rawConfig.routing && typeof rawConfig.routing === 'object'
|
|
647
|
+
&& Object.keys(rawConfig.routing).length > 0
|
|
648
|
+
? Object.keys(rawConfig.routing)
|
|
649
|
+
: null,
|
|
643
650
|
repo_run_statuses: repoRunStatuses,
|
|
644
651
|
barrier_count: barrierCount,
|
|
645
652
|
history_entries: countJsonl(files, '.agentxchain/multirepo/history.jsonl'),
|