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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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=$?
@@ -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) {