agentxchain 2.16.0 → 2.17.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/package.json
CHANGED
|
@@ -107,7 +107,7 @@ for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../exampl
|
|
|
107
107
|
(cd "$example_dir" && env -u NODE_AUTH_TOKEN -u NPM_CONFIG_USERCONFIG npm install --ignore-scripts --userconfig /dev/null 2>&1) || true
|
|
108
108
|
fi
|
|
109
109
|
done
|
|
110
|
-
if run_and_capture TEST_OUTPUT npm test; then
|
|
110
|
+
if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test; then
|
|
111
111
|
TEST_STATUS=0
|
|
112
112
|
else
|
|
113
113
|
TEST_STATUS=$?
|
package/src/commands/multi.js
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
buildEscalationPayload,
|
|
41
41
|
} from '../lib/coordinator-hooks.js';
|
|
42
42
|
import { computeContextInvalidations } from '../lib/cross-repo-context.js';
|
|
43
|
+
import { scaffoldRecoveryReport } from '../lib/workflow-gate-semantics.js';
|
|
43
44
|
|
|
44
45
|
// ── multi init ─────────────────────────────────────────────────────────────
|
|
45
46
|
|
|
@@ -601,5 +602,6 @@ function blockCoordinator(workspacePath, state, blockedReason) {
|
|
|
601
602
|
blocked_reason: blockedReason,
|
|
602
603
|
};
|
|
603
604
|
saveCoordinatorState(workspacePath, blockedState);
|
|
605
|
+
scaffoldRecoveryReport(workspacePath, blockedReason);
|
|
604
606
|
return blockedState;
|
|
605
607
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
readBarriers,
|
|
22
22
|
recordCoordinatorDecision,
|
|
23
23
|
} from './coordinator-state.js';
|
|
24
|
+
import { evaluateRecoveryReport, scaffoldRecoveryReport } from './workflow-gate-semantics.js';
|
|
24
25
|
import { safeWriteJson } from './safe-write.js';
|
|
25
26
|
import {
|
|
26
27
|
computeBarrierStatus as computeCoordinatorBarrierStatus,
|
|
@@ -416,6 +417,9 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
|
|
|
416
417
|
}
|
|
417
418
|
|
|
418
419
|
saveCoordinatorState(workspacePath, updatedState);
|
|
420
|
+
if (blockedReason) {
|
|
421
|
+
scaffoldRecoveryReport(workspacePath, blockedReason);
|
|
422
|
+
}
|
|
419
423
|
|
|
420
424
|
// Step 6: Append resync event to history
|
|
421
425
|
appendJsonl(historyPath(workspacePath), {
|
|
@@ -458,6 +462,22 @@ export function resumeCoordinatorFromBlockedState(workspacePath, state, config)
|
|
|
458
462
|
}
|
|
459
463
|
|
|
460
464
|
const previousBlockedReason = state.blocked_reason || 'unknown blocked reason';
|
|
465
|
+
|
|
466
|
+
// Require a recovery report before allowing resume
|
|
467
|
+
const reportResult = evaluateRecoveryReport(workspacePath);
|
|
468
|
+
if (reportResult === null) {
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
error: 'Recovery report required before resume. Create .agentxchain/multirepo/RECOVERY_REPORT.md with ## Trigger, ## Impact, and ## Mitigation sections.',
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (!reportResult.ok) {
|
|
475
|
+
return {
|
|
476
|
+
ok: false,
|
|
477
|
+
error: reportResult.reason,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
461
481
|
const expectedSuperRunId = state.super_run_id;
|
|
462
482
|
const resync = resyncFromRepoAuthority(workspacePath, state, config);
|
|
463
483
|
const refreshedState = loadCoordinatorState(workspacePath);
|
package/src/lib/export.js
CHANGED
|
@@ -15,6 +15,7 @@ const COORDINATOR_INCLUDED_ROOTS = [
|
|
|
15
15
|
'.agentxchain/multirepo/barriers.json',
|
|
16
16
|
'.agentxchain/multirepo/decision-ledger.jsonl',
|
|
17
17
|
'.agentxchain/multirepo/barrier-ledger.jsonl',
|
|
18
|
+
'.agentxchain/multirepo/RECOVERY_REPORT.md',
|
|
18
19
|
];
|
|
19
20
|
|
|
20
21
|
const INCLUDED_ROOTS = [
|
package/src/lib/report.js
CHANGED
|
@@ -507,6 +507,36 @@ function buildRunSubject(artifact) {
|
|
|
507
507
|
};
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
function extractRecoveryReportSection(content, heading) {
|
|
511
|
+
const pattern = new RegExp(`^${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm');
|
|
512
|
+
const match = content.match(pattern);
|
|
513
|
+
if (!match) return null;
|
|
514
|
+
const start = match.index + match[0].length;
|
|
515
|
+
const nextHeading = content.slice(start).match(/^## /m);
|
|
516
|
+
const sectionText = nextHeading
|
|
517
|
+
? content.slice(start, start + nextHeading.index).trim()
|
|
518
|
+
: content.slice(start).trim();
|
|
519
|
+
if (!sectionText || /^\(.*\)$/.test(sectionText)) return null;
|
|
520
|
+
return sectionText;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function extractRecoveryReportSummary(artifact) {
|
|
524
|
+
const entry = artifact.files?.['.agentxchain/multirepo/RECOVERY_REPORT.md'];
|
|
525
|
+
if (!entry) return null;
|
|
526
|
+
const content = typeof entry.data === 'string'
|
|
527
|
+
? entry.data
|
|
528
|
+
: (entry.content_base64 ? Buffer.from(entry.content_base64, 'base64').toString('utf8') : null);
|
|
529
|
+
if (!content) return null;
|
|
530
|
+
return {
|
|
531
|
+
present: true,
|
|
532
|
+
trigger: extractRecoveryReportSection(content, '## Trigger'),
|
|
533
|
+
impact: extractRecoveryReportSection(content, '## Impact'),
|
|
534
|
+
mitigation: extractRecoveryReportSection(content, '## Mitigation'),
|
|
535
|
+
owner: extractRecoveryReportSection(content, '## Owner'),
|
|
536
|
+
exit_condition: extractRecoveryReportSection(content, '## Exit Condition'),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
510
540
|
function buildCoordinatorSubject(artifact) {
|
|
511
541
|
const coordinatorState = extractFileData(artifact, '.agentxchain/multirepo/state.json') || {};
|
|
512
542
|
const repoStatuses = artifact.summary?.repo_run_statuses || {};
|
|
@@ -580,6 +610,7 @@ function buildCoordinatorSubject(artifact) {
|
|
|
580
610
|
barrier_summary: barrierSummary,
|
|
581
611
|
barrier_ledger_timeline: barrierLedgerTimeline,
|
|
582
612
|
decision_digest: decisionDigest,
|
|
613
|
+
recovery_report: extractRecoveryReportSummary(artifact),
|
|
583
614
|
repos,
|
|
584
615
|
artifacts: {
|
|
585
616
|
history_entries: artifact.summary?.history_entries || 0,
|
|
@@ -754,7 +785,7 @@ export function formatGovernanceReportText(report) {
|
|
|
754
785
|
return lines.join('\n');
|
|
755
786
|
}
|
|
756
787
|
|
|
757
|
-
const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest } = report.subject;
|
|
788
|
+
const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest, recovery_report } = report.subject;
|
|
758
789
|
const lines = [
|
|
759
790
|
'AgentXchain Governance Report',
|
|
760
791
|
`Input: ${report.input}`,
|
|
@@ -827,6 +858,15 @@ export function formatGovernanceReportText(report) {
|
|
|
827
858
|
}
|
|
828
859
|
}
|
|
829
860
|
|
|
861
|
+
if (recovery_report) {
|
|
862
|
+
lines.push('', 'Recovery Report:');
|
|
863
|
+
lines.push(` Trigger: ${recovery_report.trigger || 'n/a'}`);
|
|
864
|
+
lines.push(` Impact: ${recovery_report.impact || 'n/a'}`);
|
|
865
|
+
lines.push(` Mitigation: ${recovery_report.mitigation || 'n/a'}`);
|
|
866
|
+
lines.push(` Owner: ${recovery_report.owner || 'n/a'}`);
|
|
867
|
+
lines.push(` Exit Condition: ${recovery_report.exit_condition || 'n/a'}`);
|
|
868
|
+
}
|
|
869
|
+
|
|
830
870
|
lines.push('Repo details:');
|
|
831
871
|
lines.push(...repos.flatMap((repo) => {
|
|
832
872
|
if (!repo.ok) {
|
|
@@ -991,7 +1031,7 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
991
1031
|
return lines.join('\n');
|
|
992
1032
|
}
|
|
993
1033
|
|
|
994
|
-
const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest } = report.subject;
|
|
1034
|
+
const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest, recovery_report: coordRecoveryReport } = report.subject;
|
|
995
1035
|
const mdLines = [
|
|
996
1036
|
'# AgentXchain Governance Report',
|
|
997
1037
|
'',
|
|
@@ -1065,6 +1105,15 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1065
1105
|
}
|
|
1066
1106
|
}
|
|
1067
1107
|
|
|
1108
|
+
if (coordRecoveryReport) {
|
|
1109
|
+
mdLines.push('', '## Recovery Report', '');
|
|
1110
|
+
mdLines.push(`- **Trigger:** ${coordRecoveryReport.trigger || 'n/a'}`);
|
|
1111
|
+
mdLines.push(`- **Impact:** ${coordRecoveryReport.impact || 'n/a'}`);
|
|
1112
|
+
mdLines.push(`- **Mitigation:** ${coordRecoveryReport.mitigation || 'n/a'}`);
|
|
1113
|
+
mdLines.push(`- **Owner:** ${coordRecoveryReport.owner || 'n/a'}`);
|
|
1114
|
+
mdLines.push(`- **Exit Condition:** ${coordRecoveryReport.exit_condition || 'n/a'}`);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1068
1117
|
mdLines.push('', '## Repo Details', '');
|
|
1069
1118
|
mdLines.push(...repos.flatMap((repo) => {
|
|
1070
1119
|
if (!repo.ok) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
export const PM_SIGNOFF_PATH = '.planning/PM_SIGNOFF.md';
|
|
@@ -7,6 +7,7 @@ export const IMPLEMENTATION_NOTES_PATH = '.planning/IMPLEMENTATION_NOTES.md';
|
|
|
7
7
|
export const ACCEPTANCE_MATRIX_PATH = '.planning/acceptance-matrix.md';
|
|
8
8
|
export const SHIP_VERDICT_PATH = '.planning/ship-verdict.md';
|
|
9
9
|
export const RELEASE_NOTES_PATH = '.planning/RELEASE_NOTES.md';
|
|
10
|
+
export const RECOVERY_REPORT_PATH = '.agentxchain/multirepo/RECOVERY_REPORT.md';
|
|
10
11
|
|
|
11
12
|
const AFFIRMATIVE_SHIP_VERDICTS = new Set(['YES', 'SHIP', 'SHIP IT']);
|
|
12
13
|
const AFFIRMATIVE_ACCEPTANCE_STATUSES = new Set(['PASS', 'PASSED', 'OK', 'YES']);
|
|
@@ -235,6 +236,105 @@ function evaluateReleaseNotes(content) {
|
|
|
235
236
|
return { ok: true };
|
|
236
237
|
}
|
|
237
238
|
|
|
239
|
+
const RECOVERY_REPORT_PLACEHOLDER = /^\(Operator fills this before running multi resume\)$/i;
|
|
240
|
+
|
|
241
|
+
function hasRecoveryReportSectionContent(content, sectionHeader) {
|
|
242
|
+
const lines = content.split(/\r?\n/);
|
|
243
|
+
const headerIndex = lines.findIndex((line) => line.trim().startsWith(sectionHeader));
|
|
244
|
+
if (headerIndex === -1) {
|
|
245
|
+
return { found: false, hasContent: false };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (let i = headerIndex + 1; i < lines.length; i++) {
|
|
249
|
+
const line = lines[i].trim();
|
|
250
|
+
if (line.startsWith('## ')) break;
|
|
251
|
+
if (!line) continue;
|
|
252
|
+
if (RECOVERY_REPORT_PLACEHOLDER.test(line)) continue;
|
|
253
|
+
return { found: true, hasContent: true };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { found: true, hasContent: false };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function evaluateRecoveryReport(workspacePath) {
|
|
260
|
+
const content = readFile(workspacePath, RECOVERY_REPORT_PATH);
|
|
261
|
+
if (content === null) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const trigger = hasRecoveryReportSectionContent(content, '## Trigger');
|
|
266
|
+
const impact = hasRecoveryReportSectionContent(content, '## Impact');
|
|
267
|
+
const mitigation = hasRecoveryReportSectionContent(content, '## Mitigation');
|
|
268
|
+
|
|
269
|
+
const missingSections = [];
|
|
270
|
+
if (!trigger.found) missingSections.push('## Trigger');
|
|
271
|
+
if (!impact.found) missingSections.push('## Impact');
|
|
272
|
+
if (!mitigation.found) missingSections.push('## Mitigation');
|
|
273
|
+
|
|
274
|
+
if (missingSections.length > 0) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
reason: `Recovery report must define ${missingSections.join(' and ')} before resume.`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const emptySections = [];
|
|
282
|
+
if (!trigger.hasContent) emptySections.push('## Trigger');
|
|
283
|
+
if (!impact.hasContent) emptySections.push('## Impact');
|
|
284
|
+
if (!mitigation.hasContent) emptySections.push('## Mitigation');
|
|
285
|
+
|
|
286
|
+
if (emptySections.length > 0) {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
reason: `Recovery report sections still contain placeholder text: ${emptySections.join(', ')}. Replace placeholder content before running multi resume.`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { ok: true };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function scaffoldRecoveryReport(workspacePath, blockedReason) {
|
|
297
|
+
const absPath = join(workspacePath, RECOVERY_REPORT_PATH);
|
|
298
|
+
if (existsSync(absPath)) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const reasonText = typeof blockedReason === 'string'
|
|
303
|
+
? blockedReason
|
|
304
|
+
: JSON.stringify(blockedReason) || 'unknown';
|
|
305
|
+
|
|
306
|
+
const content = `# Recovery Report
|
|
307
|
+
|
|
308
|
+
Coordinator entered blocked state. Fill in the sections below before running \`agentxchain multi resume\`.
|
|
309
|
+
|
|
310
|
+
**Blocked reason:** ${reasonText}
|
|
311
|
+
**Blocked at:** ${new Date().toISOString()}
|
|
312
|
+
|
|
313
|
+
## Trigger
|
|
314
|
+
|
|
315
|
+
(Operator fills this before running multi resume)
|
|
316
|
+
|
|
317
|
+
## Impact
|
|
318
|
+
|
|
319
|
+
(Operator fills this before running multi resume)
|
|
320
|
+
|
|
321
|
+
## Mitigation
|
|
322
|
+
|
|
323
|
+
(Operator fills this before running multi resume)
|
|
324
|
+
|
|
325
|
+
## Owner
|
|
326
|
+
|
|
327
|
+
(Optional: who performed the recovery)
|
|
328
|
+
|
|
329
|
+
## Exit Condition
|
|
330
|
+
|
|
331
|
+
(Optional: what must remain true after recovery)
|
|
332
|
+
`;
|
|
333
|
+
|
|
334
|
+
writeFileSync(absPath, content);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
238
338
|
function evaluateShipVerdict(content) {
|
|
239
339
|
const verdict = parseLineValue(content, /^##\s+Verdict\s*:\s*(.+)$/im);
|
|
240
340
|
if (!verdict) {
|