agentxchain 2.130.1 → 2.131.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.130.1",
3
+ "version": "2.131.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,12 +12,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  const REPO_ROOT = join(__dirname, '..', '..');
13
13
 
14
14
  function usage() {
15
- console.error('Usage: node cli/scripts/check-release-alignment.mjs [--target-version <semver>] [--scope prebump|current] [--json]');
15
+ console.error('Usage: node cli/scripts/check-release-alignment.mjs [--target-version <semver>] [--scope prebump|current] [--json|--report]');
16
16
  }
17
17
 
18
18
  let targetVersion = null;
19
19
  let scope = RELEASE_ALIGNMENT_SCOPES.CURRENT;
20
20
  let json = false;
21
+ let report = false;
21
22
 
22
23
  for (let index = 2; index < process.argv.length; index += 1) {
23
24
  const arg = process.argv[index];
@@ -45,15 +46,40 @@ for (let index = 2; index < process.argv.length; index += 1) {
45
46
  json = true;
46
47
  continue;
47
48
  }
49
+ if (arg === '--report') {
50
+ report = true;
51
+ continue;
52
+ }
48
53
  console.error(`Error: unknown argument "${arg}"`);
49
54
  usage();
50
55
  process.exit(1);
51
56
  }
52
57
 
58
+ if (json && report) {
59
+ console.error('Error: --json and --report are mutually exclusive');
60
+ usage();
61
+ process.exit(1);
62
+ }
63
+
53
64
  const result = validateReleaseAlignment(REPO_ROOT, { targetVersion, scope });
54
65
 
55
66
  if (json) {
56
67
  console.log(JSON.stringify(result, null, 2));
68
+ } else if (report) {
69
+ const readyCount = result.surfaceResults.filter((surface) => surface.ok).length;
70
+ const needsUpdateCount = result.surfaceResults.length - readyCount;
71
+ console.log(
72
+ `Release alignment report for ${result.targetVersion} (${result.scope}, ${result.checkedSurfaceCount} surfaces).`,
73
+ );
74
+ for (const surface of result.surfaceResults) {
75
+ console.log(`- [${surface.ok ? 'ready' : 'needs update'}] (${surface.surface_id}) ${surface.label}`);
76
+ for (const error of surface.errors) {
77
+ console.log(` - ${error}`);
78
+ }
79
+ }
80
+ console.log(
81
+ `Summary: ${readyCount} ready, ${needsUpdateCount} need update.`,
82
+ );
57
83
  } else if (result.ok) {
58
84
  console.log(`Release alignment OK for ${result.targetVersion} (${result.scope}, ${result.checkedSurfaceCount} surfaces).`);
59
85
  } else {
@@ -10,11 +10,13 @@ cd "$CLI_DIR"
10
10
 
11
11
  STRICT_MODE=0
12
12
  PUBLISH_GATE=0
13
+ DRY_RUN=0
13
14
  TARGET_VERSION="2.0.0"
14
15
 
15
16
  usage() {
16
- echo "Usage: bash scripts/release-preflight.sh [--strict] [--publish-gate] [--target-version <semver>]" >&2
17
+ echo "Usage: bash scripts/release-preflight.sh [--strict] [--publish-gate] [--dry-run] [--target-version <semver>]" >&2
17
18
  echo " --publish-gate Run only release-critical checks (no full test suite). Use in CI publish workflows." >&2
19
+ echo " --dry-run Preview manual release-alignment surfaces without running the full gate." >&2
18
20
  }
19
21
 
20
22
  while [[ $# -gt 0 ]]; do
@@ -28,6 +30,10 @@ while [[ $# -gt 0 ]]; do
28
30
  STRICT_MODE=1
29
31
  shift
30
32
  ;;
33
+ --dry-run)
34
+ DRY_RUN=1
35
+ shift
36
+ ;;
31
37
  --target-version)
32
38
  if [[ -z "${2:-}" ]]; then
33
39
  echo "Error: --target-version requires a semver argument" >&2
@@ -49,6 +55,12 @@ while [[ $# -gt 0 ]]; do
49
55
  esac
50
56
  done
51
57
 
58
+ if [[ "$DRY_RUN" -eq 1 && "$STRICT_MODE" -eq 1 ]]; then
59
+ echo "Error: --dry-run cannot be combined with --strict or --publish-gate" >&2
60
+ usage
61
+ exit 1
62
+ fi
63
+
52
64
  PASS=0
53
65
  FAIL=0
54
66
  WARN=0
@@ -84,6 +96,31 @@ else
84
96
  fi
85
97
  echo ""
86
98
 
99
+ if [[ "$DRY_RUN" -eq 1 ]]; then
100
+ echo "Release Preflight Preview"
101
+ echo "========================="
102
+ echo "Mode: DRY RUN (manual release-alignment surfaces only; no git/npm gate checks executed)"
103
+ echo ""
104
+ ALIGNMENT_SCRIPT="${SCRIPT_DIR}/check-release-alignment.mjs"
105
+ if [[ ! -f "$ALIGNMENT_SCRIPT" ]]; then
106
+ echo "Error: release alignment preview requires ${ALIGNMENT_SCRIPT}" >&2
107
+ exit 1
108
+ fi
109
+ if run_and_capture ALIGNMENT_REPORT node "$ALIGNMENT_SCRIPT" --scope prebump --target-version "$TARGET_VERSION" --report; then
110
+ ALIGNMENT_STATUS=0
111
+ else
112
+ ALIGNMENT_STATUS=$?
113
+ fi
114
+ printf '%s\n' "$ALIGNMENT_REPORT"
115
+ echo ""
116
+ if [[ "$ALIGNMENT_STATUS" -eq 0 ]]; then
117
+ echo "PREVIEW COMPLETE: manual release-alignment surfaces are ready for ${TARGET_VERSION}."
118
+ else
119
+ echo "PREVIEW COMPLETE: manual release-alignment surfaces still need updates before a real preflight/tag push."
120
+ fi
121
+ exit 0
122
+ fi
123
+
87
124
  # 1. Clean working tree
88
125
  echo "[1/7] Git status"
89
126
  if git diff --quiet HEAD 2>/dev/null && [ -z "$(git ls-files --others --exclude-standard 2>/dev/null)" ]; then
@@ -22,6 +22,7 @@ import {
22
22
  getWorkstreamStatusSummary,
23
23
  launchCoordinatorWorkstream,
24
24
  launchWorkstream,
25
+ retryCoordinatorWorkstream,
25
26
  retryWorkstream,
26
27
  markWorkstreamOutcome,
27
28
  loadAllPlans,
@@ -541,11 +542,6 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
541
542
  }
542
543
 
543
544
  if (mission.coordinator && plan.coordinator_scope) {
544
- if (opts.retry) {
545
- console.error(chalk.red('--retry is not supported for coordinator-bound mission plans yet.'));
546
- process.exit(1);
547
- }
548
-
549
545
  const coordinatorConfigResult = loadCoordinatorConfig(mission.coordinator.workspace_path);
550
546
  if (!coordinatorConfigResult.ok) {
551
547
  console.error(chalk.red('Coordinator config validation failed:'));
@@ -565,6 +561,113 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
565
561
  process.exit(1);
566
562
  }
567
563
 
564
+ if (opts.retry) {
565
+ const retry = retryCoordinatorWorkstream(
566
+ root,
567
+ mission,
568
+ plan.plan_id,
569
+ opts.workstream,
570
+ coordinatorConfigResult.config,
571
+ {
572
+ reason: `mission plan retry ${opts.workstream}`,
573
+ },
574
+ );
575
+ if (!retry.ok) {
576
+ console.error(chalk.red(retry.error));
577
+ process.exit(1);
578
+ }
579
+
580
+ const executor = opts._executeGovernedRun || executeGovernedRun;
581
+ const repoContext = loadProjectContext(retry.retryResult.repo_path);
582
+ if (!repoContext) {
583
+ console.error(chalk.red(`Cannot load project context for retried repo at ${retry.retryResult.repo_path}.`));
584
+ process.exit(1);
585
+ }
586
+
587
+ const runOpts = {
588
+ autoApprove: !!opts.autoApprove,
589
+ provenance: {
590
+ trigger: 'manual',
591
+ created_by: 'operator',
592
+ trigger_reason: `mission:${mission.mission_id} workstream:${opts.workstream} coordinator-retry:${retry.retryResult.repo_id}`,
593
+ },
594
+ };
595
+
596
+ let execution;
597
+ try {
598
+ execution = await executor(repoContext, runOpts);
599
+ } catch (error) {
600
+ const syncedRetryFailure = synchronizeCoordinatorPlanState(root, mission, retry.plan);
601
+ console.error(chalk.red(`Coordinator retry execution failed: ${error.message}`));
602
+ if (opts.json) {
603
+ console.log(JSON.stringify({
604
+ dispatch_mode: 'coordinator',
605
+ retry: true,
606
+ mission_id: mission.mission_id,
607
+ plan_id: retry.plan.plan_id,
608
+ workstream_id: opts.workstream,
609
+ repo_id: retry.retryResult.repo_id,
610
+ retried_repo_turn_id: retry.retryResult.failed_turn_id,
611
+ repo_turn_id: retry.retryResult.reissued_turn_id,
612
+ workstream_status: syncedRetryFailure.ok
613
+ ? syncedRetryFailure.plan.workstreams.find((ws) => ws.workstream_id === opts.workstream)?.launch_status || 'needs_attention'
614
+ : 'needs_attention',
615
+ launch_record: retry.launchRecord,
616
+ error: error.message,
617
+ }, null, 2));
618
+ }
619
+ process.exit(1);
620
+ }
621
+
622
+ const syncedRetry = synchronizeCoordinatorPlanState(root, mission, retry.plan);
623
+ const retriedPlan = syncedRetry.ok ? syncedRetry.plan : retry.plan;
624
+ const retriedWorkstream = retriedPlan.workstreams.find((ws) => ws.workstream_id === opts.workstream);
625
+ const retriedLaunchRecord = retriedPlan.launch_records.find(
626
+ (record) => record.workstream_id === opts.workstream && record.dispatch_mode === 'coordinator',
627
+ ) || retry.launchRecord;
628
+
629
+ if (opts.json) {
630
+ console.log(JSON.stringify({
631
+ dispatch_mode: 'coordinator',
632
+ retry: true,
633
+ mission_id: mission.mission_id,
634
+ plan_id: retriedPlan.plan_id,
635
+ workstream_id: opts.workstream,
636
+ super_run_id: mission.coordinator.super_run_id,
637
+ repo_id: retry.retryResult.repo_id,
638
+ retried_repo_turn_id: retry.retryResult.failed_turn_id,
639
+ repo_turn_id: retry.retryResult.reissued_turn_id,
640
+ role: retry.retryResult.role,
641
+ bundle_path: retry.retryResult.bundle_path,
642
+ context_ref: retry.retryResult.context_ref,
643
+ workstream_status: retriedWorkstream?.launch_status || 'launched',
644
+ launch_record: retriedLaunchRecord,
645
+ exit_code: execution?.exitCode ?? 0,
646
+ }, null, 2));
647
+ if ((execution?.exitCode ?? 0) !== 0) {
648
+ process.exit(execution.exitCode);
649
+ }
650
+ return;
651
+ }
652
+
653
+ console.log(chalk.green(`Retried coordinator workstream ${chalk.bold(opts.workstream)} in ${chalk.bold(retry.retryResult.repo_id)}`));
654
+ console.log('');
655
+ console.log(chalk.dim(` Mission: ${mission.mission_id}`));
656
+ console.log(chalk.dim(` Plan: ${retriedPlan.plan_id}`));
657
+ console.log(chalk.dim(` Super Run: ${mission.coordinator.super_run_id}`));
658
+ console.log(chalk.dim(` Repo: ${retry.retryResult.repo_id}`));
659
+ console.log(chalk.dim(` Old Turn: ${retry.retryResult.failed_turn_id}`));
660
+ console.log(chalk.dim(` New Turn: ${retry.retryResult.reissued_turn_id}`));
661
+ console.log(chalk.dim(` Workstream: ${retriedWorkstream?.launch_status || 'launched'}`));
662
+ console.log('');
663
+ renderPlan(retriedPlan);
664
+ if ((execution?.exitCode ?? 0) !== 0) {
665
+ console.error(chalk.red(`Coordinator retry execution ended with exit code ${execution.exitCode}.`));
666
+ process.exit(execution.exitCode);
667
+ }
668
+ return;
669
+ }
670
+
568
671
  const assignment = selectAssignmentForWorkstream(
569
672
  mission.coordinator.workspace_path,
570
673
  coordinatorState,
@@ -85,6 +85,17 @@ function buildPlanSummary(plan) {
85
85
  completed_at: lr.completed_at || null,
86
86
  status: lr.status,
87
87
  terminal_reason: lr.terminal_reason || null,
88
+ dispatch_mode: lr.dispatch_mode || null,
89
+ ...(lr.dispatch_mode === 'coordinator' && Array.isArray(lr.repo_dispatches) ? {
90
+ repo_dispatches: lr.repo_dispatches.map((rd) => ({
91
+ repo_id: rd.repo_id,
92
+ repo_turn_id: rd.repo_turn_id,
93
+ role: rd.role,
94
+ dispatched_at: rd.dispatched_at,
95
+ ...(rd.is_retry ? { is_retry: true, retry_of: rd.retry_of } : {}),
96
+ ...(rd.retried_at ? { retried_at: rd.retried_at, retry_reason: rd.retry_reason } : {}),
97
+ })),
98
+ } : {}),
88
99
  })),
89
100
  };
90
101
  }
@@ -6,11 +6,15 @@
6
6
  * Plans are NOT protocol-normative.
7
7
  */
8
8
 
9
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
9
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
10
10
  import { randomUUID } from 'crypto';
11
11
  import { join } from 'path';
12
12
  import { loadChainReport } from './chain-reports.js';
13
- import { readBarriers, readCoordinatorHistory } from './coordinator-state.js';
13
+ import { writeDispatchBundle } from './dispatch-bundle.js';
14
+ import { loadProjectContext, loadProjectState } from './config.js';
15
+ import { reissueTurn } from './governed-state.js';
16
+ import { emitRunEvent } from './run-events.js';
17
+ import { readBarriers, readCoordinatorHistory, recordCoordinatorDecision } from './coordinator-state.js';
14
18
  import { loadCoordinatorConfig } from './coordinator-config.js';
15
19
 
16
20
  // ── Plan artifact directory ──────────────────────────────────────────────────
@@ -486,6 +490,7 @@ function isAcceptedRepoHistoryEntry(entry) {
486
490
  }
487
491
 
488
492
  const REPO_FAILURE_STATUSES = new Set(['failed_acceptance', 'failed', 'rejected', 'retrying', 'conflicted']);
493
+ const RETRYABLE_COORDINATOR_FAILURE_STATUSES = new Set(['failed', 'failed_acceptance']);
489
494
 
490
495
  function getLatestRepoDispatches(launchRecord) {
491
496
  const latestByRepo = new Map();
@@ -554,6 +559,76 @@ function buildCoordinatorRepoFailures(coordinatorConfig, launchRecord) {
554
559
  return failures;
555
560
  }
556
561
 
562
+ function appendCoordinatorHistoryEntry(workspacePath, entry) {
563
+ const historyPath = join(workspacePath, '.agentxchain', 'multirepo', 'history.jsonl');
564
+ mkdirSync(join(workspacePath, '.agentxchain', 'multirepo'), { recursive: true });
565
+ appendFileSync(historyPath, `${JSON.stringify(entry)}\n`);
566
+ }
567
+
568
+ function findDependentDispatchAfter(plan, workstreamId, timestamp) {
569
+ const since = new Date(timestamp || 0).getTime();
570
+ if (Number.isNaN(since)) return null;
571
+
572
+ for (const candidate of plan.workstreams || []) {
573
+ if (!Array.isArray(candidate.depends_on) || !candidate.depends_on.includes(workstreamId)) {
574
+ continue;
575
+ }
576
+
577
+ for (const record of plan.launch_records || []) {
578
+ if (record?.workstream_id !== candidate.workstream_id) {
579
+ continue;
580
+ }
581
+
582
+ if (record.dispatch_mode === 'coordinator') {
583
+ for (const dispatch of record.repo_dispatches || []) {
584
+ const dispatchedAt = new Date(dispatch?.dispatched_at || 0).getTime();
585
+ if (!Number.isNaN(dispatchedAt) && dispatchedAt > since) {
586
+ return {
587
+ workstream_id: candidate.workstream_id,
588
+ repo_id: dispatch.repo_id || null,
589
+ dispatched_at: dispatch.dispatched_at || null,
590
+ };
591
+ }
592
+ }
593
+ continue;
594
+ }
595
+
596
+ const launchedAt = new Date(record.launched_at || 0).getTime();
597
+ if (!Number.isNaN(launchedAt) && launchedAt > since) {
598
+ return {
599
+ workstream_id: candidate.workstream_id,
600
+ repo_id: null,
601
+ dispatched_at: record.launched_at || null,
602
+ };
603
+ }
604
+ }
605
+ }
606
+
607
+ return null;
608
+ }
609
+
610
+ function loadRepoTurnForRetry(repoPath, repoTurnId) {
611
+ const context = loadProjectContext(repoPath);
612
+ if (!context) {
613
+ return { ok: false, error: `Repo at "${repoPath}" is not a loadable governed project.` };
614
+ }
615
+
616
+ const state = loadProjectState(context.root, context.config);
617
+ if (!state) {
618
+ return { ok: false, error: `Repo at "${repoPath}" has no governed state.` };
619
+ }
620
+
621
+ const activeTurn = state.active_turns?.[repoTurnId] || null;
622
+ if (!activeTurn) {
623
+ return {
624
+ ok: false,
625
+ error: `Repo turn "${repoTurnId}" is no longer active. Use repo-local recovery instead of coordinator --retry.`,
626
+ };
627
+ }
628
+
629
+ return { ok: true, root: context.root, config: context.config, state, activeTurn };
630
+ }
631
+
557
632
  function buildCoordinatorWorkstreamProgress(coordinatorConfig, history, barriers, workstreamId) {
558
633
  const coordinatorWorkstream = coordinatorConfig?.workstreams?.[workstreamId];
559
634
  if (!coordinatorWorkstream) {
@@ -904,6 +979,188 @@ export function launchCoordinatorWorkstream(root, mission, planId, workstreamId,
904
979
  return { ok: true, plan: synced.ok ? synced.plan : plan, workstream: ws, launchRecord };
905
980
  }
906
981
 
982
+ export function retryCoordinatorWorkstream(root, mission, planId, workstreamId, coordinatorConfig, options = {}) {
983
+ const plan = loadPlan(root, mission.mission_id, planId);
984
+ if (!plan) {
985
+ return { ok: false, error: `Plan not found: ${planId}` };
986
+ }
987
+ if (!mission?.coordinator?.workspace_path || !mission?.coordinator?.super_run_id) {
988
+ return { ok: false, error: 'Mission is not bound to a coordinator run.' };
989
+ }
990
+
991
+ const synced = synchronizeCoordinatorPlanState(root, mission, plan);
992
+ const workingPlan = synced.ok ? synced.plan : plan;
993
+ const ws = workingPlan.workstreams.find((candidate) => candidate.workstream_id === workstreamId);
994
+ if (!ws) {
995
+ return { ok: false, error: `Workstream not found: ${workstreamId}` };
996
+ }
997
+ if (ws.launch_status !== 'needs_attention') {
998
+ return {
999
+ ok: false,
1000
+ error: `Workstream ${workstreamId} is not in needs_attention state. Nothing to retry.`,
1001
+ };
1002
+ }
1003
+
1004
+ const launchRecord = getLatestCoordinatorLaunchRecord(workingPlan, workstreamId);
1005
+ if (!launchRecord) {
1006
+ return { ok: false, error: `No coordinator launch record found for workstream ${workstreamId}.` };
1007
+ }
1008
+
1009
+ const latestDispatchesByRepo = new Map(
1010
+ getLatestRepoDispatches(launchRecord).map((dispatch) => [dispatch.repo_id, dispatch]),
1011
+ );
1012
+ const repoFailures = buildCoordinatorRepoFailures(coordinatorConfig, launchRecord);
1013
+ launchRecord.repo_failures = repoFailures;
1014
+
1015
+ if (repoFailures.length === 0) {
1016
+ return { ok: false, error: `Workstream ${workstreamId} has no retryable repo failures.` };
1017
+ }
1018
+
1019
+ const retryableFailures = [];
1020
+ const blockedFailures = [];
1021
+
1022
+ for (const failure of repoFailures) {
1023
+ if (!RETRYABLE_COORDINATOR_FAILURE_STATUSES.has(failure.failure_status)) {
1024
+ blockedFailures.push(`${failure.repo_id} (${failure.failure_status})`);
1025
+ continue;
1026
+ }
1027
+ retryableFailures.push(failure);
1028
+ }
1029
+
1030
+ if (retryableFailures.length === 0) {
1031
+ return {
1032
+ ok: false,
1033
+ error: `No retryable repo failures in workstream ${workstreamId}. Manual intervention required: ${blockedFailures.join(', ')}`,
1034
+ };
1035
+ }
1036
+
1037
+ if (retryableFailures.length > 1) {
1038
+ return {
1039
+ ok: false,
1040
+ error: `Workstream ${workstreamId} has multiple retryable repo failures. Recover them repo-locally one at a time before relaunching the coordinator workstream.`,
1041
+ };
1042
+ }
1043
+
1044
+ const failure = retryableFailures[0];
1045
+ const failedDispatch = latestDispatchesByRepo.get(failure.repo_id);
1046
+ if (!failedDispatch) {
1047
+ return { ok: false, error: `Missing launch metadata for failed repo ${failure.repo_id}.` };
1048
+ }
1049
+
1050
+ const downstreamBlocker = findDependentDispatchAfter(workingPlan, workstreamId, failedDispatch.dispatched_at);
1051
+ if (downstreamBlocker) {
1052
+ return {
1053
+ ok: false,
1054
+ error: `Cannot retry workstream ${workstreamId}: dependent workstream "${downstreamBlocker.workstream_id}" has already dispatched since the failed repo turn.`,
1055
+ };
1056
+ }
1057
+
1058
+ const repoPath = coordinatorConfig?.repos?.[failure.repo_id]?.resolved_path;
1059
+ if (!repoPath) {
1060
+ return { ok: false, error: `Coordinator config has no resolved_path for repo "${failure.repo_id}".` };
1061
+ }
1062
+
1063
+ const repoTurn = loadRepoTurnForRetry(repoPath, failure.repo_turn_id);
1064
+ if (!repoTurn.ok) {
1065
+ return repoTurn;
1066
+ }
1067
+
1068
+ const reason = options.reason || `coordinator retry: ${workstreamId}/${failure.repo_id}`;
1069
+ const reissued = reissueTurn(repoTurn.root, repoTurn.config, {
1070
+ turnId: failure.repo_turn_id,
1071
+ reason,
1072
+ });
1073
+ if (!reissued.ok) {
1074
+ return { ok: false, error: reissued.error };
1075
+ }
1076
+
1077
+ const bundleResult = writeDispatchBundle(repoTurn.root, reissued.state, repoTurn.config, {
1078
+ turnId: reissued.newTurn.turn_id,
1079
+ });
1080
+ if (!bundleResult.ok) {
1081
+ return { ok: false, error: `Turn reissued but dispatch bundle failed: ${bundleResult.error}` };
1082
+ }
1083
+
1084
+ const now = new Date().toISOString();
1085
+ failedDispatch.retried_at = now;
1086
+ failedDispatch.retry_reason = failure.failure_status;
1087
+ launchRecord.repo_dispatches.push({
1088
+ repo_id: failure.repo_id,
1089
+ repo_turn_id: reissued.newTurn.turn_id,
1090
+ role: reissued.newTurn.assigned_role,
1091
+ dispatched_at: now,
1092
+ bundle_path: bundleResult.bundlePath,
1093
+ context_ref: failedDispatch.context_ref || null,
1094
+ is_retry: true,
1095
+ retry_of: failure.repo_turn_id,
1096
+ });
1097
+ launchRecord.status = 'launched';
1098
+ launchRecord.repo_failures = [];
1099
+ ws.launch_status = 'launched';
1100
+
1101
+ const otherNeedsAttention = (workingPlan.workstreams || []).some(
1102
+ (candidate) => candidate.workstream_id !== workstreamId && candidate.launch_status === 'needs_attention',
1103
+ );
1104
+ workingPlan.status = otherNeedsAttention ? 'needs_attention' : 'approved';
1105
+ workingPlan.updated_at = now;
1106
+ writePlanArtifact(root, mission.mission_id, workingPlan);
1107
+
1108
+ appendCoordinatorHistoryEntry(mission.coordinator.workspace_path, {
1109
+ type: 'coordinator_retry',
1110
+ timestamp: now,
1111
+ super_run_id: mission.coordinator.super_run_id,
1112
+ workstream_id: workstreamId,
1113
+ repo_id: failure.repo_id,
1114
+ failed_turn_id: failure.repo_turn_id,
1115
+ reissued_turn_id: reissued.newTurn.turn_id,
1116
+ retry_reason: failure.failure_status,
1117
+ });
1118
+ recordCoordinatorDecision(mission.coordinator.workspace_path, {
1119
+ super_run_id: mission.coordinator.super_run_id,
1120
+ phase: coordinatorConfig?.workstreams?.[workstreamId]?.phase || null,
1121
+ }, {
1122
+ category: 'retry',
1123
+ statement: `Retried ${failure.repo_id} for workstream ${workstreamId}`,
1124
+ repo_id: failure.repo_id,
1125
+ repo_turn_id: reissued.newTurn.turn_id,
1126
+ workstream_id: workstreamId,
1127
+ reason: failure.failure_status,
1128
+ context_ref: failedDispatch.context_ref || null,
1129
+ });
1130
+ emitRunEvent(mission.coordinator.workspace_path, 'coordinator_retry', {
1131
+ run_id: mission.coordinator.super_run_id,
1132
+ phase: coordinatorConfig?.workstreams?.[workstreamId]?.phase || null,
1133
+ status: 'active',
1134
+ turn: { turn_id: reissued.newTurn.turn_id, role_id: reissued.newTurn.assigned_role },
1135
+ payload: {
1136
+ workstream_id: workstreamId,
1137
+ repo_id: failure.repo_id,
1138
+ failed_turn_id: failure.repo_turn_id,
1139
+ reissued_turn_id: reissued.newTurn.turn_id,
1140
+ retry_reason: failure.failure_status,
1141
+ retry_count: launchRecord.repo_dispatches.filter((dispatch) => dispatch.repo_id === failure.repo_id && dispatch.is_retry).length,
1142
+ },
1143
+ });
1144
+
1145
+ const afterRetry = synchronizeCoordinatorPlanState(root, mission, workingPlan);
1146
+ return {
1147
+ ok: true,
1148
+ plan: afterRetry.ok ? afterRetry.plan : workingPlan,
1149
+ workstream: ws,
1150
+ launchRecord,
1151
+ retryResult: {
1152
+ repo_id: failure.repo_id,
1153
+ failed_turn_id: failure.repo_turn_id,
1154
+ reissued_turn_id: reissued.newTurn.turn_id,
1155
+ role: reissued.newTurn.assigned_role,
1156
+ repo_path: repoTurn.root,
1157
+ bundle_path: bundleResult.bundlePath,
1158
+ context_ref: failedDispatch.context_ref || null,
1159
+ retry_reason: failure.failure_status,
1160
+ },
1161
+ };
1162
+ }
1163
+
907
1164
  /**
908
1165
  * Record the outcome of a launched workstream after its chain completes.
909
1166
  *
@@ -311,9 +311,23 @@ export function validateReleaseAlignment(repoRoot, { targetVersion, scope = RELE
311
311
  const context = getReleaseAlignmentContext(repoRoot, { targetVersion });
312
312
  const surfaces = RELEASE_ALIGNMENT_SURFACES.filter((surface) => surface.scopes.includes(scope));
313
313
  const errors = [];
314
+ const surfaceResults = [];
314
315
 
315
316
  for (const surface of surfaces) {
316
- const surfaceErrors = surface.check(context, repoRoot) || [];
317
+ let surfaceErrors = [];
318
+ try {
319
+ surfaceErrors = surface.check(context, repoRoot) || [];
320
+ } catch (error) {
321
+ surfaceErrors = [
322
+ error instanceof Error ? error.message : String(error),
323
+ ];
324
+ }
325
+ surfaceResults.push({
326
+ surface_id: surface.id,
327
+ label: surface.label,
328
+ ok: surfaceErrors.length === 0,
329
+ errors: surfaceErrors,
330
+ });
317
331
  for (const error of surfaceErrors) {
318
332
  errors.push({
319
333
  surface_id: surface.id,
@@ -331,6 +345,12 @@ export function validateReleaseAlignment(repoRoot, { targetVersion, scope = RELE
331
345
  aggregateEvidenceLine: context.aggregateEvidenceLine,
332
346
  checkedSurfaceCount: surfaces.length,
333
347
  checkedSurfaceIds: surfaces.map((surface) => surface.id),
348
+ checkedSurfaces: surfaces.map((surface) => ({
349
+ id: surface.id,
350
+ label: surface.label,
351
+ scopes: [...surface.scopes],
352
+ })),
353
+ surfaceResults,
334
354
  errors,
335
355
  };
336
356
  }
@@ -21,6 +21,7 @@ export const VALID_RUN_EVENTS = [
21
21
  'acceptance_failed',
22
22
  'turn_reissued',
23
23
  'turn_checkpointed',
24
+ 'coordinator_retry',
24
25
  'run_blocked',
25
26
  'run_completed',
26
27
  'escalation_raised',