agentxchain 2.130.1 → 2.132.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-bump.sh +3 -0
- package/scripts/release-preflight.sh +38 -1
- package/src/commands/events.js +21 -1
- package/src/commands/mission.js +108 -5
- package/src/lib/dashboard/plan-reader.js +11 -0
- package/src/lib/governed-state.js +191 -56
- package/src/lib/mission-plans.js +259 -2
- package/src/lib/recent-event-summary.js +15 -0
- package/src/lib/release-alignment.js +55 -1
- package/src/lib/run-events.js +2 -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 {
|
package/scripts/release-bump.sh
CHANGED
|
@@ -82,6 +82,9 @@ ALLOWED_RELEASE_PATHS=(
|
|
|
82
82
|
".planning/MARKETING/REDDIT_POSTS.md"
|
|
83
83
|
".planning/MARKETING/HN_SUBMISSION.md"
|
|
84
84
|
"website-v2/static/llms.txt"
|
|
85
|
+
"website-v2/docs/getting-started.mdx"
|
|
86
|
+
"website-v2/docs/quickstart.mdx"
|
|
87
|
+
"website-v2/docs/five-minute-tutorial.mdx"
|
|
85
88
|
"cli/homebrew/agentxchain.rb"
|
|
86
89
|
"cli/homebrew/README.md"
|
|
87
90
|
)
|
|
@@ -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/events.js
CHANGED
|
@@ -65,6 +65,9 @@ function printEvent(evt) {
|
|
|
65
65
|
const conflictDetail = evt.event_type === 'turn_conflicted'
|
|
66
66
|
? ` — ${formatConflictDetail(evt)}`
|
|
67
67
|
: '';
|
|
68
|
+
const conflictResolvedDetail = evt.event_type === 'conflict_resolved'
|
|
69
|
+
? ` — ${formatConflictResolvedDetail(evt)}`
|
|
70
|
+
: '';
|
|
68
71
|
const rejectionDetail = evt.event_type === 'turn_rejected' && evt.payload?.reason
|
|
69
72
|
? ` — ${evt.payload.reason}${evt.payload.failed_stage ? ` (${evt.payload.failed_stage})` : ''}`
|
|
70
73
|
: '';
|
|
@@ -82,7 +85,10 @@ function printEvent(evt) {
|
|
|
82
85
|
: evt.event_type === 'human_escalation_resolved' && evt.payload?.escalation_id
|
|
83
86
|
? ` ${evt.payload.escalation_id} via ${evt.payload.resolved_via || '?'}`
|
|
84
87
|
: '';
|
|
85
|
-
|
|
88
|
+
const coordinatorRetryDetail = evt.event_type === 'coordinator_retry' && evt.payload
|
|
89
|
+
? ` — ws ${evt.payload.workstream_id || '?'} repo ${evt.payload.repo_id || '?'} (retry of ${evt.payload.failed_turn_id || '?'})`
|
|
90
|
+
: '';
|
|
91
|
+
console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${intentInfo}${conflictDetail}${conflictResolvedDetail}${rejectionDetail}${acceptanceFailedDetail}${phaseTransitionDetail}${gateFailedDetail}${humanEscalationDetail}${coordinatorRetryDetail}`);
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
function formatConflictDetail(evt) {
|
|
@@ -105,6 +111,16 @@ function formatConflictDetail(evt) {
|
|
|
105
111
|
return parts.filter(Boolean).join(' | ');
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
function formatConflictResolvedDetail(evt) {
|
|
115
|
+
const payload = evt.payload || {};
|
|
116
|
+
const fileSummary = summarizeList(payload.conflicting_files, 3) || 'resolved conflict';
|
|
117
|
+
const resolvedVia = payload.resolution ? `via ${payload.resolution}` : null;
|
|
118
|
+
const overlapRatio = typeof payload.overlap_ratio === 'number'
|
|
119
|
+
? `${Math.round(payload.overlap_ratio * 100)}% overlap`
|
|
120
|
+
: null;
|
|
121
|
+
return [fileSummary, resolvedVia, overlapRatio].filter(Boolean).join(' | ');
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
function summarizeList(items, limit) {
|
|
109
125
|
if (!Array.isArray(items) || items.length === 0) return '';
|
|
110
126
|
const shown = items.slice(0, limit).join(', ');
|
|
@@ -123,6 +139,7 @@ function colorEventType(type) {
|
|
|
123
139
|
acceptance_failed: chalk.red.bold,
|
|
124
140
|
turn_reissued: chalk.cyan,
|
|
125
141
|
turn_conflicted: chalk.redBright,
|
|
142
|
+
conflict_resolved: chalk.greenBright,
|
|
126
143
|
phase_entered: chalk.magenta,
|
|
127
144
|
escalation_raised: chalk.red.bold,
|
|
128
145
|
escalation_resolved: chalk.green,
|
|
@@ -132,6 +149,9 @@ function colorEventType(type) {
|
|
|
132
149
|
gate_approved: chalk.green,
|
|
133
150
|
gate_failed: chalk.red,
|
|
134
151
|
budget_exceeded_warn: chalk.yellowBright,
|
|
152
|
+
coordinator_retry: chalk.cyan.bold,
|
|
153
|
+
turn_checkpointed: chalk.green,
|
|
154
|
+
dispatch_progress: chalk.blue.dim,
|
|
135
155
|
};
|
|
136
156
|
const colorFn = colors[type] || chalk.white;
|
|
137
157
|
return colorFn(pad(type, 22));
|
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
|
}
|
|
@@ -1088,15 +1088,63 @@ function cleanupTurnArtifacts(root, turnId) {
|
|
|
1088
1088
|
} catch { /* best-effort */ }
|
|
1089
1089
|
}
|
|
1090
1090
|
|
|
1091
|
-
function
|
|
1091
|
+
function getWorkflowArtifactOwners(config, filePath) {
|
|
1092
|
+
const owners = new Set();
|
|
1093
|
+
const phases = config?.workflow_kit?.phases;
|
|
1094
|
+
if (!phases || typeof phases !== 'object') {
|
|
1095
|
+
return owners;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
for (const phaseConfig of Object.values(phases)) {
|
|
1099
|
+
const artifacts = Array.isArray(phaseConfig?.artifacts) ? phaseConfig.artifacts : [];
|
|
1100
|
+
for (const artifact of artifacts) {
|
|
1101
|
+
if (artifact?.path !== filePath) {
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (typeof artifact.owned_by === 'string' && artifact.owned_by.length > 0) {
|
|
1105
|
+
owners.add(artifact.owned_by);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return owners;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function isForwardRevisionFile(targetTurn, historyEntry, filePath, config) {
|
|
1114
|
+
if (!targetTurn || !historyEntry) {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
if (historyEntry.role !== targetTurn.assigned_role) {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const explicitOwners = getWorkflowArtifactOwners(config, filePath);
|
|
1122
|
+
if (explicitOwners.size > 0) {
|
|
1123
|
+
return explicitOwners.size === 1 && explicitOwners.has(targetTurn.assigned_role);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (!filePath.startsWith('.planning/')) {
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
return config?.routing?.planning?.entry_role === targetTurn.assigned_role;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function classifyAcceptanceOverlap(targetTurn, conflictFiles, historyEntries, config) {
|
|
1092
1134
|
const observedFiles = [...new Set(Array.isArray(conflictFiles) ? conflictFiles : [])];
|
|
1093
1135
|
if (observedFiles.length === 0) {
|
|
1094
|
-
return
|
|
1136
|
+
return {
|
|
1137
|
+
conflict: null,
|
|
1138
|
+
forward_revision_files: [],
|
|
1139
|
+
forward_revision_turns: [],
|
|
1140
|
+
};
|
|
1095
1141
|
}
|
|
1096
1142
|
|
|
1097
1143
|
const observedFileSet = new Set(observedFiles);
|
|
1098
1144
|
const acceptedSince = [];
|
|
1099
1145
|
const conflictingFiles = new Set();
|
|
1146
|
+
const forwardRevisionFiles = new Set();
|
|
1147
|
+
const forwardRevisionTurns = new Map();
|
|
1100
1148
|
|
|
1101
1149
|
for (const entry of historyEntries) {
|
|
1102
1150
|
if ((entry.accepted_sequence || 0) <= (targetTurn.assigned_sequence || 0)) {
|
|
@@ -1108,35 +1156,77 @@ function detectAcceptanceConflict(targetTurn, conflictFiles, historyEntries) {
|
|
|
1108
1156
|
continue;
|
|
1109
1157
|
}
|
|
1110
1158
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1159
|
+
const destructiveFiles = [];
|
|
1160
|
+
const forwardFiles = [];
|
|
1161
|
+
for (const file of overlap) {
|
|
1162
|
+
if (isForwardRevisionFile(targetTurn, entry, file, config)) {
|
|
1163
|
+
forwardFiles.push(file);
|
|
1164
|
+
} else {
|
|
1165
|
+
destructiveFiles.push(file);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (destructiveFiles.length > 0) {
|
|
1170
|
+
destructiveFiles.forEach(file => conflictingFiles.add(file));
|
|
1171
|
+
acceptedSince.push({
|
|
1172
|
+
turn_id: entry.turn_id,
|
|
1173
|
+
role: entry.role,
|
|
1174
|
+
accepted_sequence: entry.accepted_sequence,
|
|
1175
|
+
files_changed: destructiveFiles,
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (forwardFiles.length > 0) {
|
|
1180
|
+
forwardFiles.forEach(file => forwardRevisionFiles.add(file));
|
|
1181
|
+
const existing = forwardRevisionTurns.get(entry.turn_id) || {
|
|
1182
|
+
turn_id: entry.turn_id,
|
|
1183
|
+
role: entry.role,
|
|
1184
|
+
accepted_sequence: entry.accepted_sequence,
|
|
1185
|
+
files_changed: [],
|
|
1186
|
+
};
|
|
1187
|
+
existing.files_changed = [...new Set([...existing.files_changed, ...forwardFiles])];
|
|
1188
|
+
forwardRevisionTurns.set(entry.turn_id, existing);
|
|
1189
|
+
}
|
|
1118
1190
|
}
|
|
1119
1191
|
|
|
1192
|
+
const forwardRevisionContext = {
|
|
1193
|
+
files: [...forwardRevisionFiles],
|
|
1194
|
+
accepted_since_turn_ids: [...forwardRevisionTurns.values()].map((entry) => entry.turn_id),
|
|
1195
|
+
accepted_since: [...forwardRevisionTurns.values()],
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1120
1198
|
if (acceptedSince.length === 0) {
|
|
1121
|
-
return
|
|
1199
|
+
return {
|
|
1200
|
+
conflict: null,
|
|
1201
|
+
forward_revision_files: forwardRevisionContext.files,
|
|
1202
|
+
forward_revision_turns: forwardRevisionContext.accepted_since,
|
|
1203
|
+
};
|
|
1122
1204
|
}
|
|
1123
1205
|
|
|
1124
1206
|
const conflicting = [...conflictingFiles];
|
|
1125
1207
|
const overlapRatio = observedFiles.length > 0 ? conflicting.length / observedFiles.length : 0;
|
|
1126
1208
|
|
|
1127
1209
|
return {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1210
|
+
conflict: {
|
|
1211
|
+
type: 'file_conflict',
|
|
1212
|
+
conflicting_turn: {
|
|
1213
|
+
turn_id: targetTurn.turn_id,
|
|
1214
|
+
role: targetTurn.assigned_role,
|
|
1215
|
+
attempt: targetTurn.attempt,
|
|
1216
|
+
files_changed: observedFiles,
|
|
1217
|
+
},
|
|
1218
|
+
accepted_since: acceptedSince,
|
|
1219
|
+
conflicting_files: conflicting,
|
|
1220
|
+
non_conflicting_files: observedFiles.filter(
|
|
1221
|
+
(file) => !conflictingFiles.has(file) && !forwardRevisionFiles.has(file),
|
|
1222
|
+
),
|
|
1223
|
+
forward_revision_files: forwardRevisionContext.files,
|
|
1224
|
+
forward_revision_turns: forwardRevisionContext.accepted_since,
|
|
1225
|
+
overlap_ratio: overlapRatio,
|
|
1226
|
+
suggested_resolution: overlapRatio < 0.5 ? 'reject_and_reassign' : 'human_merge',
|
|
1134
1227
|
},
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
non_conflicting_files: observedFiles.filter(file => !conflictingFiles.has(file)),
|
|
1138
|
-
overlap_ratio: overlapRatio,
|
|
1139
|
-
suggested_resolution: overlapRatio < 0.5 ? 'reject_and_reassign' : 'human_merge',
|
|
1228
|
+
forward_revision_files: forwardRevisionContext.files,
|
|
1229
|
+
forward_revision_turns: forwardRevisionContext.accepted_since,
|
|
1140
1230
|
};
|
|
1141
1231
|
}
|
|
1142
1232
|
|
|
@@ -2749,40 +2839,6 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2749
2839
|
error_code: 'protocol_error',
|
|
2750
2840
|
};
|
|
2751
2841
|
}
|
|
2752
|
-
|
|
2753
|
-
if (currentTurn.conflict_state.status !== 'human_merging') {
|
|
2754
|
-
appendJsonl(root, LEDGER_PATH, {
|
|
2755
|
-
timestamp: new Date().toISOString(),
|
|
2756
|
-
decision: 'conflict_resolution_selected',
|
|
2757
|
-
turn_id: currentTurn.turn_id,
|
|
2758
|
-
attempt: currentTurn.attempt,
|
|
2759
|
-
role: currentTurn.assigned_role,
|
|
2760
|
-
phase: state.phase,
|
|
2761
|
-
conflict: {
|
|
2762
|
-
conflicting_files: currentTurn.conflict_state.conflict_error?.conflicting_files || [],
|
|
2763
|
-
accepted_since_turn_ids: (currentTurn.conflict_state.conflict_error?.accepted_since || []).map((entry) => entry.turn_id),
|
|
2764
|
-
overlap_ratio: currentTurn.conflict_state.conflict_error?.overlap_ratio ?? 0,
|
|
2765
|
-
},
|
|
2766
|
-
resolution_chosen: 'human_merge',
|
|
2767
|
-
});
|
|
2768
|
-
|
|
2769
|
-
state = {
|
|
2770
|
-
...state,
|
|
2771
|
-
active_turns: {
|
|
2772
|
-
...getActiveTurns(state),
|
|
2773
|
-
[currentTurn.turn_id]: {
|
|
2774
|
-
...currentTurn,
|
|
2775
|
-
status: 'conflicted',
|
|
2776
|
-
conflict_state: {
|
|
2777
|
-
...currentTurn.conflict_state,
|
|
2778
|
-
status: 'human_merging',
|
|
2779
|
-
},
|
|
2780
|
-
},
|
|
2781
|
-
},
|
|
2782
|
-
};
|
|
2783
|
-
writeState(root, state);
|
|
2784
|
-
currentTurn = state.active_turns[currentTurn.turn_id];
|
|
2785
|
-
}
|
|
2786
2842
|
}
|
|
2787
2843
|
|
|
2788
2844
|
const turnStagingPath = getTurnStagingResultPath(currentTurn.turn_id);
|
|
@@ -3142,11 +3198,29 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3142
3198
|
};
|
|
3143
3199
|
}
|
|
3144
3200
|
|
|
3145
|
-
const
|
|
3201
|
+
const overlapClassification = classifyAcceptanceOverlap(
|
|
3146
3202
|
currentTurn,
|
|
3147
3203
|
buildConflictCandidateFiles(rawObservation, observation, turnResult.files_changed || []),
|
|
3148
3204
|
historyEntries,
|
|
3205
|
+
config,
|
|
3149
3206
|
);
|
|
3207
|
+
const forwardRevision = overlapClassification.forward_revision_files.length > 0
|
|
3208
|
+
? {
|
|
3209
|
+
files: overlapClassification.forward_revision_files,
|
|
3210
|
+
accepted_since_turn_ids: overlapClassification.forward_revision_turns.map((entry) => entry.turn_id),
|
|
3211
|
+
accepted_since: overlapClassification.forward_revision_turns,
|
|
3212
|
+
}
|
|
3213
|
+
: null;
|
|
3214
|
+
const conflict = resolutionMode === 'human_merge' ? null : overlapClassification.conflict;
|
|
3215
|
+
const conflictResolution = resolutionMode === 'human_merge'
|
|
3216
|
+
? {
|
|
3217
|
+
mode: 'human_merge',
|
|
3218
|
+
merge_strategy: 'operator_authoritative_staged_result',
|
|
3219
|
+
conflicting_files: currentTurn.conflict_state?.conflict_error?.conflicting_files || [],
|
|
3220
|
+
accepted_since_turn_ids: (currentTurn.conflict_state?.conflict_error?.accepted_since || []).map((entry) => entry.turn_id),
|
|
3221
|
+
overlap_ratio: currentTurn.conflict_state?.conflict_error?.overlap_ratio ?? 0,
|
|
3222
|
+
}
|
|
3223
|
+
: null;
|
|
3150
3224
|
|
|
3151
3225
|
if (conflict) {
|
|
3152
3226
|
const detectionCount = (currentTurn.conflict_state?.detection_count || 0) + 1;
|
|
@@ -3301,6 +3375,8 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3301
3375
|
assigned_sequence: Number.isInteger(currentTurn.assigned_sequence) ? currentTurn.assigned_sequence : acceptedSequence,
|
|
3302
3376
|
accepted_sequence: acceptedSequence,
|
|
3303
3377
|
concurrent_with: Array.isArray(currentTurn.concurrent_with) ? currentTurn.concurrent_with : [],
|
|
3378
|
+
...(forwardRevision ? { forward_revision: forwardRevision } : {}),
|
|
3379
|
+
...(conflictResolution ? { conflict_resolution: conflictResolution } : {}),
|
|
3304
3380
|
cost: turnResult.cost || {},
|
|
3305
3381
|
...(currentTurn.started_at ? { started_at: currentTurn.started_at } : {}),
|
|
3306
3382
|
accepted_at: now,
|
|
@@ -3357,6 +3433,49 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3357
3433
|
});
|
|
3358
3434
|
}
|
|
3359
3435
|
}
|
|
3436
|
+
if (forwardRevision) {
|
|
3437
|
+
ledgerEntries.push({
|
|
3438
|
+
timestamp: now,
|
|
3439
|
+
decision: 'forward_revision_accepted',
|
|
3440
|
+
turn_id: currentTurn.turn_id,
|
|
3441
|
+
attempt: currentTurn.attempt,
|
|
3442
|
+
role: currentTurn.assigned_role,
|
|
3443
|
+
phase: state.phase,
|
|
3444
|
+
forward_revision: {
|
|
3445
|
+
files: forwardRevision.files,
|
|
3446
|
+
accepted_since_turn_ids: forwardRevision.accepted_since_turn_ids,
|
|
3447
|
+
},
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
if (conflictResolution) {
|
|
3451
|
+
const conflictSummary = {
|
|
3452
|
+
conflicting_files: conflictResolution.conflicting_files,
|
|
3453
|
+
accepted_since_turn_ids: conflictResolution.accepted_since_turn_ids,
|
|
3454
|
+
overlap_ratio: conflictResolution.overlap_ratio,
|
|
3455
|
+
};
|
|
3456
|
+
ledgerEntries.push({
|
|
3457
|
+
timestamp: now,
|
|
3458
|
+
decision: 'conflict_resolution_selected',
|
|
3459
|
+
turn_id: currentTurn.turn_id,
|
|
3460
|
+
attempt: currentTurn.attempt,
|
|
3461
|
+
role: currentTurn.assigned_role,
|
|
3462
|
+
phase: state.phase,
|
|
3463
|
+
conflict: conflictSummary,
|
|
3464
|
+
resolution_chosen: conflictResolution.mode,
|
|
3465
|
+
merge_strategy: conflictResolution.merge_strategy,
|
|
3466
|
+
});
|
|
3467
|
+
ledgerEntries.push({
|
|
3468
|
+
timestamp: now,
|
|
3469
|
+
decision: 'conflict_resolved',
|
|
3470
|
+
turn_id: currentTurn.turn_id,
|
|
3471
|
+
attempt: currentTurn.attempt,
|
|
3472
|
+
role: currentTurn.assigned_role,
|
|
3473
|
+
phase: state.phase,
|
|
3474
|
+
conflict: conflictSummary,
|
|
3475
|
+
resolution_chosen: conflictResolution.mode,
|
|
3476
|
+
merge_strategy: conflictResolution.merge_strategy,
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3360
3479
|
|
|
3361
3480
|
const turnNumber = turnResult.turn_id.replace(/^turn_/, '').slice(0, 8);
|
|
3362
3481
|
const talkSection = `## Turn ${turnNumber} — ${turnResult.role} (${state.phase})\n\n- **Status:** ${turnResult.status}\n- **Summary:** ${turnResult.summary}\n${turnResult.decisions?.length ? turnResult.decisions.map(d => `- **Decision ${d.id}:** ${d.statement}`).join('\n') + '\n' : ''}${turnResult.objections?.length ? turnResult.objections.map(o => `- **Objection ${o.id} (${o.severity}):** ${o.statement}`).join('\n') + '\n' : ''}- **Proposed next:** ${turnResult.proposed_next_role || 'human'}\n\n---\n`;
|
|
@@ -4107,6 +4226,22 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
4107
4226
|
intent_id: currentTurn.intake_context?.intent_id || null,
|
|
4108
4227
|
payload: turnAcceptedPayload,
|
|
4109
4228
|
});
|
|
4229
|
+
if (conflictResolution) {
|
|
4230
|
+
emitRunEvent(root, 'conflict_resolved', {
|
|
4231
|
+
run_id: updatedState.run_id,
|
|
4232
|
+
phase: updatedState.phase,
|
|
4233
|
+
status: updatedState.status,
|
|
4234
|
+
turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
|
|
4235
|
+
intent_id: currentTurn.intake_context?.intent_id || null,
|
|
4236
|
+
payload: {
|
|
4237
|
+
resolution: conflictResolution.mode,
|
|
4238
|
+
merge_strategy: conflictResolution.merge_strategy,
|
|
4239
|
+
conflicting_files: conflictResolution.conflicting_files,
|
|
4240
|
+
accepted_since_turn_ids: conflictResolution.accepted_since_turn_ids,
|
|
4241
|
+
overlap_ratio: conflictResolution.overlap_ratio,
|
|
4242
|
+
},
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4110
4245
|
|
|
4111
4246
|
if (updatedState.status === 'blocked') {
|
|
4112
4247
|
// DEC-RHTR-SPEC: Record blocked outcome in cross-run history (non-fatal)
|
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
|
*
|
|
@@ -36,6 +36,21 @@ function describeEvent(eventType, entry) {
|
|
|
36
36
|
case 'gate_approved':
|
|
37
37
|
case 'gate_failed':
|
|
38
38
|
return `${prefix}${eventType}${gateId ? ` (${gateId})` : ''}`;
|
|
39
|
+
case 'turn_conflicted':
|
|
40
|
+
return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
|
|
41
|
+
case 'conflict_resolved': {
|
|
42
|
+
const resolution = trimToNull(entry.payload?.resolution);
|
|
43
|
+
return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}${resolution ? ` via ${resolution}` : ''}`;
|
|
44
|
+
}
|
|
45
|
+
case 'coordinator_retry': {
|
|
46
|
+
const wsId = trimToNull(entry.payload?.workstream_id);
|
|
47
|
+
const retryRepo = trimToNull(entry.payload?.repo_id);
|
|
48
|
+
return `${prefix}${eventType}${wsId ? ` ${wsId}` : ''}${retryRepo ? ` (${retryRepo})` : ''}`;
|
|
49
|
+
}
|
|
50
|
+
case 'turn_checkpointed':
|
|
51
|
+
return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
|
|
52
|
+
case 'dispatch_progress':
|
|
53
|
+
return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
|
|
39
54
|
case 'run_blocked':
|
|
40
55
|
case 'run_completed':
|
|
41
56
|
case 'run_started':
|
|
@@ -22,6 +22,12 @@ export function formatCount(value) {
|
|
|
22
22
|
return new Intl.NumberFormat('en-US').format(value);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const ONBOARDING_PREREQ_DOCS = [
|
|
26
|
+
'website-v2/docs/getting-started.mdx',
|
|
27
|
+
'website-v2/docs/quickstart.mdx',
|
|
28
|
+
'website-v2/docs/five-minute-tutorial.mdx',
|
|
29
|
+
];
|
|
30
|
+
|
|
25
31
|
function normalizeEvidenceText(value) {
|
|
26
32
|
return value
|
|
27
33
|
.replace(/^\s*-\s*/, '')
|
|
@@ -137,6 +143,28 @@ function validateTextIncludesVersionAndEvidence(relativePath, label) {
|
|
|
137
143
|
};
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
function validateOnboardingPrereqs(ctx, repoRoot) {
|
|
147
|
+
const errors = [];
|
|
148
|
+
const requiredTokens = [
|
|
149
|
+
`Minimum CLI version: \`agentxchain ${ctx.targetVersion}\` or newer`,
|
|
150
|
+
'agentxchain --version',
|
|
151
|
+
'npm install -g agentxchain@latest',
|
|
152
|
+
'brew upgrade agentxchain',
|
|
153
|
+
'npx --yes -p agentxchain@latest -c "agentxchain <command>"',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (const relativePath of ONBOARDING_PREREQ_DOCS) {
|
|
157
|
+
const content = read(repoRoot, relativePath);
|
|
158
|
+
for (const token of requiredTokens) {
|
|
159
|
+
if (!content.includes(token)) {
|
|
160
|
+
errors.push(`${relativePath} must include: ${token}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return errors;
|
|
166
|
+
}
|
|
167
|
+
|
|
140
168
|
export const RELEASE_ALIGNMENT_SURFACES = [
|
|
141
169
|
{
|
|
142
170
|
id: 'changelog',
|
|
@@ -274,6 +302,12 @@ export const RELEASE_ALIGNMENT_SURFACES = [
|
|
|
274
302
|
: [`website-v2/static/llms.txt must list ${ctx.releaseRoute}`];
|
|
275
303
|
},
|
|
276
304
|
},
|
|
305
|
+
{
|
|
306
|
+
id: 'onboarding_prereqs',
|
|
307
|
+
label: 'onboarding docs CLI-version prerequisites',
|
|
308
|
+
scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
|
|
309
|
+
check: validateOnboardingPrereqs,
|
|
310
|
+
},
|
|
277
311
|
{
|
|
278
312
|
id: 'homebrew_formula_url',
|
|
279
313
|
label: 'homebrew mirror formula url',
|
|
@@ -311,9 +345,23 @@ export function validateReleaseAlignment(repoRoot, { targetVersion, scope = RELE
|
|
|
311
345
|
const context = getReleaseAlignmentContext(repoRoot, { targetVersion });
|
|
312
346
|
const surfaces = RELEASE_ALIGNMENT_SURFACES.filter((surface) => surface.scopes.includes(scope));
|
|
313
347
|
const errors = [];
|
|
348
|
+
const surfaceResults = [];
|
|
314
349
|
|
|
315
350
|
for (const surface of surfaces) {
|
|
316
|
-
|
|
351
|
+
let surfaceErrors = [];
|
|
352
|
+
try {
|
|
353
|
+
surfaceErrors = surface.check(context, repoRoot) || [];
|
|
354
|
+
} catch (error) {
|
|
355
|
+
surfaceErrors = [
|
|
356
|
+
error instanceof Error ? error.message : String(error),
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
surfaceResults.push({
|
|
360
|
+
surface_id: surface.id,
|
|
361
|
+
label: surface.label,
|
|
362
|
+
ok: surfaceErrors.length === 0,
|
|
363
|
+
errors: surfaceErrors,
|
|
364
|
+
});
|
|
317
365
|
for (const error of surfaceErrors) {
|
|
318
366
|
errors.push({
|
|
319
367
|
surface_id: surface.id,
|
|
@@ -331,6 +379,12 @@ export function validateReleaseAlignment(repoRoot, { targetVersion, scope = RELE
|
|
|
331
379
|
aggregateEvidenceLine: context.aggregateEvidenceLine,
|
|
332
380
|
checkedSurfaceCount: surfaces.length,
|
|
333
381
|
checkedSurfaceIds: surfaces.map((surface) => surface.id),
|
|
382
|
+
checkedSurfaces: surfaces.map((surface) => ({
|
|
383
|
+
id: surface.id,
|
|
384
|
+
label: surface.label,
|
|
385
|
+
scopes: [...surface.scopes],
|
|
386
|
+
})),
|
|
387
|
+
surfaceResults,
|
|
334
388
|
errors,
|
|
335
389
|
};
|
|
336
390
|
}
|
package/src/lib/run-events.js
CHANGED
|
@@ -18,9 +18,11 @@ export const VALID_RUN_EVENTS = [
|
|
|
18
18
|
'turn_accepted',
|
|
19
19
|
'turn_rejected',
|
|
20
20
|
'turn_conflicted',
|
|
21
|
+
'conflict_resolved',
|
|
21
22
|
'acceptance_failed',
|
|
22
23
|
'turn_reissued',
|
|
23
24
|
'turn_checkpointed',
|
|
25
|
+
'coordinator_retry',
|
|
24
26
|
'run_blocked',
|
|
25
27
|
'run_completed',
|
|
26
28
|
'escalation_raised',
|