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 +1 -1
- package/scripts/check-release-alignment.mjs +27 -1
- package/scripts/release-preflight.sh +38 -1
- package/src/commands/mission.js +108 -5
- package/src/lib/dashboard/plan-reader.js +11 -0
- package/src/lib/mission-plans.js +259 -2
- package/src/lib/release-alignment.js +21 -1
- package/src/lib/run-events.js +1 -0
package/package.json
CHANGED
|
@@ -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
|
package/src/commands/mission.js
CHANGED
|
@@ -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
|
}
|
package/src/lib/mission-plans.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
}
|