agentxchain 2.128.0 → 2.130.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/README.md +2 -0
- package/bin/agentxchain.js +38 -4
- package/package.json +1 -1
- package/scripts/verify-post-publish.sh +55 -5
- package/src/commands/accept-turn.js +14 -0
- package/src/commands/checkpoint-turn.js +35 -0
- package/src/commands/connector.js +17 -2
- package/src/commands/doctor.js +151 -1
- package/src/commands/events.js +7 -1
- package/src/commands/init.js +42 -11
- package/src/commands/inject.js +1 -1
- package/src/commands/mission.js +803 -7
- package/src/commands/reissue-turn.js +122 -0
- package/src/commands/reject-turn.js +60 -6
- package/src/commands/restart.js +81 -10
- package/src/commands/resume.js +20 -9
- package/src/commands/run.js +13 -0
- package/src/commands/status.js +58 -4
- package/src/commands/step.js +49 -10
- package/src/commands/validate.js +78 -20
- package/src/lib/cli-version.js +106 -0
- package/src/lib/connector-probe.js +146 -5
- package/src/lib/continuous-run.js +22 -87
- package/src/lib/coordinator-dispatch.js +25 -0
- package/src/lib/dispatch-bundle.js +39 -0
- package/src/lib/governed-state.js +624 -11
- package/src/lib/governed-templates.js +1 -0
- package/src/lib/intake.js +233 -77
- package/src/lib/mission-plans.js +510 -6
- package/src/lib/missions.js +65 -6
- package/src/lib/normalized-config.js +50 -15
- package/src/lib/repo-observer.js +8 -2
- package/src/lib/run-events.js +5 -0
- package/src/lib/run-loop.js +25 -0
- package/src/lib/runner-interface.js +2 -0
- package/src/lib/session-checkpoint.js +18 -2
- package/src/lib/turn-checkpoint.js +221 -0
- package/src/templates/governed/full-local-cli.json +71 -0
package/src/lib/mission-plans.js
CHANGED
|
@@ -10,6 +10,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
|
|
|
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';
|
|
14
|
+
import { loadCoordinatorConfig } from './coordinator-config.js';
|
|
13
15
|
|
|
14
16
|
// ── Plan artifact directory ──────────────────────────────────────────────────
|
|
15
17
|
|
|
@@ -140,20 +142,81 @@ export function validatePlannerOutput(output) {
|
|
|
140
142
|
|
|
141
143
|
// ── Plan artifact creation ───────────────────────────────────────────────────
|
|
142
144
|
|
|
145
|
+
// ── Coordinator phase alignment ─────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate that plan workstream phases align with coordinator config phases.
|
|
149
|
+
* Returns { ok: true } or { ok: false, errors: string[] }.
|
|
150
|
+
*/
|
|
151
|
+
export function validatePlanCoordinatorPhaseAlignment(workstreams, coordinatorConfig) {
|
|
152
|
+
if (!coordinatorConfig) return { ok: true };
|
|
153
|
+
|
|
154
|
+
const errors = [];
|
|
155
|
+
const coordinatorPhases = coordinatorConfig.routing
|
|
156
|
+
? new Set(Object.keys(coordinatorConfig.routing))
|
|
157
|
+
: new Set(['planning', 'implementation', 'qa']);
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < workstreams.length; i++) {
|
|
160
|
+
const ws = workstreams[i];
|
|
161
|
+
if (!Array.isArray(ws.phases)) continue;
|
|
162
|
+
for (const phase of ws.phases) {
|
|
163
|
+
if (!coordinatorPhases.has(phase)) {
|
|
164
|
+
errors.push(
|
|
165
|
+
`workstreams[${i}] ("${ws.workstream_id}"): phase "${phase}" is not defined in coordinator config. ` +
|
|
166
|
+
`Valid phases: ${[...coordinatorPhases].join(', ')}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return errors.length > 0 ? { ok: false, errors } : { ok: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build coordinator_scope metadata for a plan artifact.
|
|
177
|
+
*/
|
|
178
|
+
function buildCoordinatorScope(mission, coordinatorConfig) {
|
|
179
|
+
if (!mission.coordinator || !coordinatorConfig) return null;
|
|
180
|
+
|
|
181
|
+
const repoIds = coordinatorConfig.repos ? Object.keys(coordinatorConfig.repos) : [];
|
|
182
|
+
const phases = coordinatorConfig.routing
|
|
183
|
+
? Object.keys(coordinatorConfig.routing)
|
|
184
|
+
: ['planning', 'implementation', 'qa'];
|
|
185
|
+
const coordinatorWorkstreams = coordinatorConfig.workstreams
|
|
186
|
+
? Object.keys(coordinatorConfig.workstreams)
|
|
187
|
+
: [];
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
super_run_id: mission.coordinator.super_run_id || null,
|
|
191
|
+
repo_ids: repoIds,
|
|
192
|
+
phases,
|
|
193
|
+
coordinator_workstream_ids: coordinatorWorkstreams,
|
|
194
|
+
bound_at: new Date().toISOString(),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
143
198
|
/**
|
|
144
199
|
* Create a plan artifact from validated planner output.
|
|
145
200
|
*
|
|
146
201
|
* @param {string} root - project root
|
|
147
202
|
* @param {object} mission - mission artifact (must have mission_id, goal)
|
|
148
|
-
* @param {object} options - { constraints, roleHints, plannerOutput }
|
|
203
|
+
* @param {object} options - { constraints, roleHints, plannerOutput, coordinatorConfig }
|
|
149
204
|
* @returns {{ ok: boolean, plan?: object, errors?: string[] }}
|
|
150
205
|
*/
|
|
151
|
-
export function createPlanArtifact(root, mission, { constraints = [], roleHints = [], plannerOutput }) {
|
|
206
|
+
export function createPlanArtifact(root, mission, { constraints = [], roleHints = [], plannerOutput, coordinatorConfig = null }) {
|
|
152
207
|
const validation = validatePlannerOutput(plannerOutput);
|
|
153
208
|
if (!validation.ok) {
|
|
154
209
|
return { ok: false, errors: validation.errors };
|
|
155
210
|
}
|
|
156
211
|
|
|
212
|
+
// Validate phase alignment with coordinator when mission is coordinator-bound
|
|
213
|
+
if (coordinatorConfig) {
|
|
214
|
+
const phaseCheck = validatePlanCoordinatorPhaseAlignment(validation.workstreams, coordinatorConfig);
|
|
215
|
+
if (!phaseCheck.ok) {
|
|
216
|
+
return { ok: false, errors: phaseCheck.errors };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
157
220
|
const missionId = mission.mission_id;
|
|
158
221
|
const existingPlans = loadAllPlans(root, missionId);
|
|
159
222
|
const supersedes = existingPlans[0] || null;
|
|
@@ -173,6 +236,8 @@ export function createPlanArtifact(root, mission, { constraints = [], roleHints
|
|
|
173
236
|
launch_status: Array.isArray(ws.depends_on) && ws.depends_on.length > 0 ? 'blocked' : 'ready',
|
|
174
237
|
}));
|
|
175
238
|
|
|
239
|
+
const coordinatorScope = buildCoordinatorScope(mission, coordinatorConfig);
|
|
240
|
+
|
|
176
241
|
const plan = {
|
|
177
242
|
plan_id: planId,
|
|
178
243
|
mission_id: missionId,
|
|
@@ -189,6 +254,7 @@ export function createPlanArtifact(root, mission, { constraints = [], roleHints
|
|
|
189
254
|
mode: 'llm_one_shot',
|
|
190
255
|
model: 'configured mission planner',
|
|
191
256
|
},
|
|
257
|
+
...(coordinatorScope ? { coordinator_scope: coordinatorScope } : {}),
|
|
192
258
|
workstreams,
|
|
193
259
|
launch_records: [],
|
|
194
260
|
};
|
|
@@ -314,7 +380,7 @@ export function buildPlanProgressSummary(plan) {
|
|
|
314
380
|
const workstreamStatusCounts = getWorkstreamStatusSummary(plan);
|
|
315
381
|
const completedCount = workstreamStatusCounts.completed || 0;
|
|
316
382
|
|
|
317
|
-
|
|
383
|
+
const summary = {
|
|
318
384
|
plan_id: plan.plan_id,
|
|
319
385
|
mission_id: plan.mission_id,
|
|
320
386
|
status: plan.status,
|
|
@@ -336,6 +402,14 @@ export function buildPlanProgressSummary(plan) {
|
|
|
336
402
|
? 0
|
|
337
403
|
: Math.round((completedCount / workstreams.length) * 100),
|
|
338
404
|
};
|
|
405
|
+
|
|
406
|
+
if (plan.coordinator_scope) {
|
|
407
|
+
summary.coordinator_bound = true;
|
|
408
|
+
summary.coordinator_repo_count = (plan.coordinator_scope.repo_ids || []).length;
|
|
409
|
+
summary.coordinator_phases = plan.coordinator_scope.phases || [];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return summary;
|
|
339
413
|
}
|
|
340
414
|
|
|
341
415
|
// ── Workstream launch ───────────────────────────────────────────────────────
|
|
@@ -349,6 +423,303 @@ export function didChainFinishSuccessfully(chainReport) {
|
|
|
349
423
|
return lastRun?.status === 'completed';
|
|
350
424
|
}
|
|
351
425
|
|
|
426
|
+
function getCoordinatorCompletionBarrierId(workstreamId) {
|
|
427
|
+
return `${workstreamId}_completion`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function getAcceptedRepoIdsFromHistory(history, workstreamId) {
|
|
431
|
+
return [
|
|
432
|
+
...new Set(
|
|
433
|
+
history
|
|
434
|
+
.filter((entry) => entry?.type === 'acceptance_projection' && entry.workstream_id === workstreamId && entry.repo_id)
|
|
435
|
+
.map((entry) => entry.repo_id),
|
|
436
|
+
),
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getLatestLaunchRecord(plan, workstreamId) {
|
|
441
|
+
const records = Array.isArray(plan.launch_records) ? plan.launch_records : [];
|
|
442
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
443
|
+
if (records[i]?.workstream_id === workstreamId) {
|
|
444
|
+
return records[i];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function getLatestCoordinatorLaunchRecord(plan, workstreamId) {
|
|
451
|
+
const records = Array.isArray(plan.launch_records) ? plan.launch_records : [];
|
|
452
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
453
|
+
if (records[i]?.workstream_id === workstreamId && records[i]?.dispatch_mode === 'coordinator') {
|
|
454
|
+
return records[i];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function clonePlan(plan) {
|
|
461
|
+
return JSON.parse(JSON.stringify(plan));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function readRepoLocalState(repoPath) {
|
|
465
|
+
if (!repoPath) return null;
|
|
466
|
+
try {
|
|
467
|
+
return JSON.parse(readFileSync(join(repoPath, '.agentxchain', 'state.json'), 'utf8'));
|
|
468
|
+
} catch {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function readRepoLocalHistory(repoPath) {
|
|
474
|
+
if (!repoPath) return [];
|
|
475
|
+
try {
|
|
476
|
+
const content = readFileSync(join(repoPath, '.agentxchain', 'history.jsonl'), 'utf8').trim();
|
|
477
|
+
if (!content) return [];
|
|
478
|
+
return content.split('\n').map((line) => JSON.parse(line));
|
|
479
|
+
} catch {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function isAcceptedRepoHistoryEntry(entry) {
|
|
485
|
+
return Boolean(entry?.accepted_at) || entry?.status === 'accepted';
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const REPO_FAILURE_STATUSES = new Set(['failed_acceptance', 'failed', 'rejected', 'retrying', 'conflicted']);
|
|
489
|
+
|
|
490
|
+
function getLatestRepoDispatches(launchRecord) {
|
|
491
|
+
const latestByRepo = new Map();
|
|
492
|
+
for (const dispatch of launchRecord?.repo_dispatches || []) {
|
|
493
|
+
if (dispatch?.repo_id) {
|
|
494
|
+
latestByRepo.set(dispatch.repo_id, dispatch);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return [...latestByRepo.values()];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function classifyRepoDispatchOutcome(repoPath, repoTurnId) {
|
|
501
|
+
const repoState = readRepoLocalState(repoPath);
|
|
502
|
+
const activeTurn = repoState?.active_turns?.[repoTurnId] || null;
|
|
503
|
+
if (activeTurn) {
|
|
504
|
+
if (REPO_FAILURE_STATUSES.has(activeTurn.status)) {
|
|
505
|
+
return {
|
|
506
|
+
status: 'failed',
|
|
507
|
+
source: 'repo_state',
|
|
508
|
+
failure_status: activeTurn.status,
|
|
509
|
+
failure_reason: activeTurn.failure_reason || activeTurn.last_rejection?.reason || null,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return { status: 'in_flight', source: 'repo_state', failure_status: null, failure_reason: null };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const repoHistory = readRepoLocalHistory(repoPath);
|
|
516
|
+
const matchingEntries = repoHistory.filter((entry) => entry?.turn_id === repoTurnId);
|
|
517
|
+
if (matchingEntries.some((entry) => isAcceptedRepoHistoryEntry(entry))) {
|
|
518
|
+
return { status: 'accepted', source: 'repo_history', failure_status: null, failure_reason: null };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
for (let i = matchingEntries.length - 1; i >= 0; i--) {
|
|
522
|
+
const entry = matchingEntries[i];
|
|
523
|
+
if (REPO_FAILURE_STATUSES.has(entry?.status)) {
|
|
524
|
+
return {
|
|
525
|
+
status: 'failed',
|
|
526
|
+
source: 'repo_history',
|
|
527
|
+
failure_status: entry.status,
|
|
528
|
+
failure_reason: entry.failure_reason || entry.reason || entry.summary || null,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return { status: 'unknown', source: 'repo_state', failure_status: null, failure_reason: null };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function buildCoordinatorRepoFailures(coordinatorConfig, launchRecord) {
|
|
537
|
+
const failures = [];
|
|
538
|
+
for (const dispatch of getLatestRepoDispatches(launchRecord)) {
|
|
539
|
+
const repoPath = coordinatorConfig?.repos?.[dispatch.repo_id]?.resolved_path || null;
|
|
540
|
+
const outcome = classifyRepoDispatchOutcome(repoPath, dispatch.repo_turn_id);
|
|
541
|
+
if (outcome.status !== 'failed') {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
failures.push({
|
|
546
|
+
repo_id: dispatch.repo_id,
|
|
547
|
+
repo_turn_id: dispatch.repo_turn_id,
|
|
548
|
+
role: dispatch.role || null,
|
|
549
|
+
failure_status: outcome.failure_status,
|
|
550
|
+
failure_reason: outcome.failure_reason,
|
|
551
|
+
detected_from: outcome.source,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return failures;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function buildCoordinatorWorkstreamProgress(coordinatorConfig, history, barriers, workstreamId) {
|
|
558
|
+
const coordinatorWorkstream = coordinatorConfig?.workstreams?.[workstreamId];
|
|
559
|
+
if (!coordinatorWorkstream) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const acceptedRepoIds = getAcceptedRepoIdsFromHistory(history, workstreamId);
|
|
564
|
+
const allRepos = Array.isArray(coordinatorWorkstream.repos) ? coordinatorWorkstream.repos : [];
|
|
565
|
+
const pendingRepoIds = allRepos.filter((repoId) => !acceptedRepoIds.includes(repoId));
|
|
566
|
+
const barrierId = getCoordinatorCompletionBarrierId(workstreamId);
|
|
567
|
+
const barrier = barriers?.[barrierId] || null;
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
repo_ids: allRepos,
|
|
571
|
+
repo_count: allRepos.length,
|
|
572
|
+
accepted_repo_ids: acceptedRepoIds,
|
|
573
|
+
accepted_repo_count: acceptedRepoIds.length,
|
|
574
|
+
pending_repo_ids: pendingRepoIds,
|
|
575
|
+
completion_barrier_id: barrierId,
|
|
576
|
+
completion_barrier_type: coordinatorWorkstream.completion_barrier || barrier?.type || null,
|
|
577
|
+
completion_barrier_status: barrier?.status || (pendingRepoIds.length === 0 ? 'satisfied' : 'pending'),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function synchronizeCoordinatorWorkstreamStatuses(root, plan, coordinatorConfig, history, barriers) {
|
|
582
|
+
let changed = false;
|
|
583
|
+
|
|
584
|
+
for (const ws of plan.workstreams || []) {
|
|
585
|
+
const progress = buildCoordinatorWorkstreamProgress(coordinatorConfig, history, barriers, ws.workstream_id);
|
|
586
|
+
if (!progress) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const launchRecord = getLatestCoordinatorLaunchRecord(plan, ws.workstream_id);
|
|
591
|
+
const repoFailures = launchRecord
|
|
592
|
+
? buildCoordinatorRepoFailures(coordinatorConfig, launchRecord)
|
|
593
|
+
: [];
|
|
594
|
+
if (launchRecord) {
|
|
595
|
+
launchRecord.accepted_repo_ids = [...progress.accepted_repo_ids];
|
|
596
|
+
launchRecord.pending_repo_ids = [...progress.pending_repo_ids];
|
|
597
|
+
launchRecord.repo_count = progress.repo_count;
|
|
598
|
+
launchRecord.accepted_repo_count = progress.accepted_repo_count;
|
|
599
|
+
launchRecord.repo_failures = repoFailures;
|
|
600
|
+
launchRecord.completion_barrier = {
|
|
601
|
+
barrier_id: progress.completion_barrier_id,
|
|
602
|
+
type: progress.completion_barrier_type,
|
|
603
|
+
status: progress.completion_barrier_status,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (progress.completion_barrier_status === 'satisfied') {
|
|
608
|
+
if (ws.launch_status !== 'completed') {
|
|
609
|
+
ws.launch_status = 'completed';
|
|
610
|
+
changed = true;
|
|
611
|
+
}
|
|
612
|
+
if (launchRecord && launchRecord.status !== 'completed') {
|
|
613
|
+
launchRecord.status = 'completed';
|
|
614
|
+
launchRecord.completed_at = launchRecord.completed_at || new Date().toISOString();
|
|
615
|
+
changed = true;
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (repoFailures.length > 0) {
|
|
621
|
+
if (ws.launch_status !== 'needs_attention') {
|
|
622
|
+
ws.launch_status = 'needs_attention';
|
|
623
|
+
changed = true;
|
|
624
|
+
}
|
|
625
|
+
if (launchRecord && launchRecord.status !== 'needs_attention') {
|
|
626
|
+
launchRecord.status = 'needs_attention';
|
|
627
|
+
changed = true;
|
|
628
|
+
}
|
|
629
|
+
if (plan.status !== 'needs_attention') {
|
|
630
|
+
plan.status = 'needs_attention';
|
|
631
|
+
changed = true;
|
|
632
|
+
}
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if ((launchRecord?.repo_dispatches?.length || 0) > 0 || progress.accepted_repo_count > 0) {
|
|
637
|
+
if (ws.launch_status !== 'launched') {
|
|
638
|
+
ws.launch_status = 'launched';
|
|
639
|
+
changed = true;
|
|
640
|
+
}
|
|
641
|
+
if (launchRecord && launchRecord.status !== 'launched') {
|
|
642
|
+
launchRecord.status = 'launched';
|
|
643
|
+
changed = true;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
for (const ws of plan.workstreams || []) {
|
|
649
|
+
if (ws.launch_status !== 'blocked') {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const stillBlocked = checkDependencySatisfaction(plan, ws, root);
|
|
653
|
+
if (stillBlocked.length === 0) {
|
|
654
|
+
ws.launch_status = 'ready';
|
|
655
|
+
changed = true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const allCompleted = Array.isArray(plan.workstreams) && plan.workstreams.length > 0
|
|
660
|
+
&& plan.workstreams.every((ws) => ws.launch_status === 'completed');
|
|
661
|
+
if (allCompleted && plan.status !== 'completed') {
|
|
662
|
+
plan.status = 'completed';
|
|
663
|
+
changed = true;
|
|
664
|
+
} else if (plan.status === 'needs_attention' && !(plan.workstreams || []).some((ws) => ws.launch_status === 'needs_attention')) {
|
|
665
|
+
plan.status = 'approved';
|
|
666
|
+
changed = true;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (changed) {
|
|
670
|
+
plan.updated_at = new Date().toISOString();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return changed;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function synchronizeCoordinatorPlanState(root, mission, plan) {
|
|
677
|
+
if (!mission?.coordinator?.workspace_path || !plan?.coordinator_scope) {
|
|
678
|
+
return { ok: true, plan };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const workspacePath = mission.coordinator.workspace_path;
|
|
682
|
+
const history = readCoordinatorHistory(workspacePath);
|
|
683
|
+
const barriers = readBarriers(workspacePath);
|
|
684
|
+
const coordinatorConfigResult = loadCoordinatorConfig(workspacePath);
|
|
685
|
+
if (!coordinatorConfigResult.ok) {
|
|
686
|
+
return { ok: false, error: `Coordinator config validation failed at ${workspacePath}: ${(coordinatorConfigResult.errors || []).join('; ')}` };
|
|
687
|
+
}
|
|
688
|
+
const coordinatorConfig = coordinatorConfigResult.config;
|
|
689
|
+
|
|
690
|
+
const persistedPlan = clonePlan(plan);
|
|
691
|
+
const changed = synchronizeCoordinatorWorkstreamStatuses(root, persistedPlan, coordinatorConfig, history, barriers);
|
|
692
|
+
if (changed) {
|
|
693
|
+
writePlanArtifact(root, mission.mission_id, persistedPlan);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const enrichedPlan = clonePlan(persistedPlan);
|
|
697
|
+
for (const ws of enrichedPlan.workstreams || []) {
|
|
698
|
+
const progress = buildCoordinatorWorkstreamProgress(coordinatorConfig, history, barriers, ws.workstream_id);
|
|
699
|
+
if (!progress) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const launchRecord = getLatestCoordinatorLaunchRecord(enrichedPlan, ws.workstream_id);
|
|
703
|
+
const repoFailures = launchRecord?.repo_failures || [];
|
|
704
|
+
ws.coordinator_progress = progress;
|
|
705
|
+
ws.coordinator_progress.failed_repo_ids = repoFailures.map((failure) => failure.repo_id);
|
|
706
|
+
ws.coordinator_progress.repo_failure_count = repoFailures.length;
|
|
707
|
+
if (repoFailures.length > 0) {
|
|
708
|
+
ws.coordinator_progress.repo_failures = repoFailures;
|
|
709
|
+
}
|
|
710
|
+
if (launchRecord) {
|
|
711
|
+
launchRecord.coordinator_progress = progress;
|
|
712
|
+
launchRecord.coordinator_progress.failed_repo_ids = ws.coordinator_progress.failed_repo_ids;
|
|
713
|
+
launchRecord.coordinator_progress.repo_failure_count = repoFailures.length;
|
|
714
|
+
if (repoFailures.length > 0) {
|
|
715
|
+
launchRecord.coordinator_progress.repo_failures = repoFailures;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return { ok: true, plan: enrichedPlan, changed };
|
|
721
|
+
}
|
|
722
|
+
|
|
352
723
|
/**
|
|
353
724
|
* Check whether a workstream's dependencies are satisfied.
|
|
354
725
|
* A dependency is satisfied when its launch_record exists AND the bound chain's
|
|
@@ -361,11 +732,22 @@ export function checkDependencySatisfaction(plan, workstream, root) {
|
|
|
361
732
|
if (!Array.isArray(workstream.depends_on)) return unsatisfied;
|
|
362
733
|
|
|
363
734
|
for (const depId of workstream.depends_on) {
|
|
735
|
+
const dependencyWorkstream = (plan.workstreams || []).find((candidate) => candidate.workstream_id === depId);
|
|
736
|
+
if (dependencyWorkstream?.launch_status === 'completed') {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
|
|
364
740
|
const depRecord = (plan.launch_records || []).find((r) => r.workstream_id === depId);
|
|
365
741
|
if (!depRecord) {
|
|
366
742
|
unsatisfied.push(depId);
|
|
367
743
|
continue;
|
|
368
744
|
}
|
|
745
|
+
if (depRecord.dispatch_mode === 'coordinator') {
|
|
746
|
+
if (depRecord.status !== 'completed') {
|
|
747
|
+
unsatisfied.push(depId);
|
|
748
|
+
}
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
369
751
|
// Check that the dependency chain actually completed
|
|
370
752
|
const chainReport = loadChainReport(root, depRecord.chain_id);
|
|
371
753
|
if (!didChainFinishSuccessfully(chainReport)) {
|
|
@@ -421,6 +803,7 @@ export function launchWorkstream(root, missionId, planId, workstreamId, options
|
|
|
421
803
|
const now = new Date().toISOString();
|
|
422
804
|
const launchRecord = {
|
|
423
805
|
workstream_id: workstreamId,
|
|
806
|
+
dispatch_mode: 'chain',
|
|
424
807
|
chain_id: chainId,
|
|
425
808
|
launched_at: now,
|
|
426
809
|
status: 'launched',
|
|
@@ -438,6 +821,89 @@ export function launchWorkstream(root, missionId, planId, workstreamId, options
|
|
|
438
821
|
return { ok: true, plan, workstream: ws, chainId, launchRecord };
|
|
439
822
|
}
|
|
440
823
|
|
|
824
|
+
export function launchCoordinatorWorkstream(root, mission, planId, workstreamId, dispatchResult, coordinatorConfig) {
|
|
825
|
+
const plan = loadPlan(root, mission.mission_id, planId);
|
|
826
|
+
if (!plan) {
|
|
827
|
+
return { ok: false, error: `Plan not found: ${planId}` };
|
|
828
|
+
}
|
|
829
|
+
if (!mission?.coordinator?.super_run_id) {
|
|
830
|
+
return { ok: false, error: 'Mission is not bound to a coordinator run.' };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const allowNeedsAttention = dispatchResult?.allowNeedsAttention === true;
|
|
834
|
+
if (plan.status !== 'approved' && !(allowNeedsAttention && plan.status === 'needs_attention')) {
|
|
835
|
+
return {
|
|
836
|
+
ok: false,
|
|
837
|
+
error: `Plan ${planId} is not approved (status: "${plan.status}"). Approve the plan before launching workstreams.`,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const ws = plan.workstreams.find((candidate) => candidate.workstream_id === workstreamId);
|
|
842
|
+
if (!ws) {
|
|
843
|
+
return { ok: false, error: `Workstream not found: ${workstreamId}` };
|
|
844
|
+
}
|
|
845
|
+
if (ws.launch_status === 'completed') {
|
|
846
|
+
return { ok: false, error: `Workstream ${workstreamId} is already completed.` };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const coordinatorWorkstream = coordinatorConfig?.workstreams?.[workstreamId];
|
|
850
|
+
if (!coordinatorWorkstream) {
|
|
851
|
+
return { ok: false, error: `Coordinator config does not declare workstream ${workstreamId}.` };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const unsatisfied = checkDependencySatisfaction(plan, ws, root);
|
|
855
|
+
if (unsatisfied.length > 0) {
|
|
856
|
+
return {
|
|
857
|
+
ok: false,
|
|
858
|
+
error: `Workstream ${workstreamId} has unsatisfied dependencies: ${unsatisfied.join(', ')}. Launch and complete those workstreams first.`,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const now = new Date().toISOString();
|
|
863
|
+
let launchRecord = getLatestCoordinatorLaunchRecord(plan, workstreamId);
|
|
864
|
+
if (!launchRecord || launchRecord.status === 'completed' || launchRecord.status === 'failed') {
|
|
865
|
+
launchRecord = {
|
|
866
|
+
workstream_id: workstreamId,
|
|
867
|
+
dispatch_mode: 'coordinator',
|
|
868
|
+
super_run_id: mission.coordinator.super_run_id,
|
|
869
|
+
launched_at: now,
|
|
870
|
+
status: 'launched',
|
|
871
|
+
completion_barrier: {
|
|
872
|
+
barrier_id: getCoordinatorCompletionBarrierId(workstreamId),
|
|
873
|
+
type: coordinatorWorkstream.completion_barrier || null,
|
|
874
|
+
},
|
|
875
|
+
repo_dispatches: [],
|
|
876
|
+
};
|
|
877
|
+
if (!Array.isArray(plan.launch_records)) {
|
|
878
|
+
plan.launch_records = [];
|
|
879
|
+
}
|
|
880
|
+
plan.launch_records.push(launchRecord);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
launchRecord.status = 'launched';
|
|
884
|
+
if (!Array.isArray(launchRecord.repo_dispatches)) {
|
|
885
|
+
launchRecord.repo_dispatches = [];
|
|
886
|
+
}
|
|
887
|
+
launchRecord.repo_dispatches.push({
|
|
888
|
+
repo_id: dispatchResult.repo_id,
|
|
889
|
+
repo_turn_id: dispatchResult.turn_id,
|
|
890
|
+
role: dispatchResult.role,
|
|
891
|
+
dispatched_at: now,
|
|
892
|
+
bundle_path: dispatchResult.bundle_path,
|
|
893
|
+
context_ref: dispatchResult.context_ref || null,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
ws.launch_status = 'launched';
|
|
897
|
+
if (plan.status === 'needs_attention') {
|
|
898
|
+
plan.status = 'approved';
|
|
899
|
+
}
|
|
900
|
+
plan.updated_at = now;
|
|
901
|
+
writePlanArtifact(root, mission.mission_id, plan);
|
|
902
|
+
|
|
903
|
+
const synced = synchronizeCoordinatorPlanState(root, mission, plan);
|
|
904
|
+
return { ok: true, plan: synced.ok ? synced.plan : plan, workstream: ws, launchRecord };
|
|
905
|
+
}
|
|
906
|
+
|
|
441
907
|
/**
|
|
442
908
|
* Record the outcome of a launched workstream after its chain completes.
|
|
443
909
|
*
|
|
@@ -529,6 +995,7 @@ export function retryWorkstream(root, missionId, planId, workstreamId, options =
|
|
|
529
995
|
const now = new Date().toISOString();
|
|
530
996
|
const launchRecord = {
|
|
531
997
|
workstream_id: workstreamId,
|
|
998
|
+
dispatch_mode: 'chain',
|
|
532
999
|
chain_id: chainId,
|
|
533
1000
|
launched_at: now,
|
|
534
1001
|
status: 'launched',
|
|
@@ -579,9 +1046,16 @@ export function getWorkstreamStatusSummary(plan) {
|
|
|
579
1046
|
|
|
580
1047
|
/**
|
|
581
1048
|
* Build the system+user prompt for the mission planner LLM call.
|
|
1049
|
+
*
|
|
1050
|
+
* @param {object} mission - mission artifact
|
|
1051
|
+
* @param {string[]} constraints - user constraints
|
|
1052
|
+
* @param {string[]} roleHints - available role names
|
|
1053
|
+
* @param {object} [coordinatorConfig] - coordinator config when mission is multi-repo
|
|
582
1054
|
*/
|
|
583
|
-
export function buildPlannerPrompt(mission, constraints, roleHints) {
|
|
584
|
-
const
|
|
1055
|
+
export function buildPlannerPrompt(mission, constraints, roleHints, coordinatorConfig = null) {
|
|
1056
|
+
const isMultiRepo = !!coordinatorConfig;
|
|
1057
|
+
|
|
1058
|
+
let systemPrompt = `You are a mission decomposition planner for AgentXchain, a governed multi-agent software delivery system.
|
|
585
1059
|
|
|
586
1060
|
Given a mission goal, optional constraints, and optional role hints, produce a JSON object with a single "workstreams" array.
|
|
587
1061
|
|
|
@@ -599,7 +1073,24 @@ Rules:
|
|
|
599
1073
|
- Do NOT include chain_id — chain IDs are runtime artifacts.
|
|
600
1074
|
- Keep workstream count between 2 and 8.
|
|
601
1075
|
- Each workstream should be a meaningful delivery slice, not a single task.
|
|
602
|
-
- Use concrete, testable acceptance checks
|
|
1076
|
+
- Use concrete, testable acceptance checks.`;
|
|
1077
|
+
|
|
1078
|
+
if (isMultiRepo) {
|
|
1079
|
+
const validPhases = coordinatorConfig.routing
|
|
1080
|
+
? Object.keys(coordinatorConfig.routing)
|
|
1081
|
+
: ['planning', 'implementation', 'qa'];
|
|
1082
|
+
const repoIds = coordinatorConfig.repos ? Object.keys(coordinatorConfig.repos) : [];
|
|
1083
|
+
|
|
1084
|
+
systemPrompt += `
|
|
1085
|
+
|
|
1086
|
+
Multi-repo coordinator context:
|
|
1087
|
+
- This mission spans multiple repositories: ${repoIds.join(', ')}.
|
|
1088
|
+
- Valid phases are: ${validPhases.join(', ')}. Use ONLY these phases in workstream phase arrays.
|
|
1089
|
+
- Workstreams should account for cross-repo coordination needs (interface alignment, shared decisions, phased rollout).
|
|
1090
|
+
- Prefer workstreams that map cleanly to coordinator barrier types (all_repos_accepted, interface_alignment, named_decisions).`;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
systemPrompt += `
|
|
603
1094
|
|
|
604
1095
|
Respond with ONLY valid JSON. No markdown, no explanation.`;
|
|
605
1096
|
|
|
@@ -611,6 +1102,19 @@ Respond with ONLY valid JSON. No markdown, no explanation.`;
|
|
|
611
1102
|
parts.push(`Available roles: ${roleHints.join(', ')}`);
|
|
612
1103
|
}
|
|
613
1104
|
|
|
1105
|
+
if (isMultiRepo) {
|
|
1106
|
+
const repoEntries = Object.entries(coordinatorConfig.repos || {});
|
|
1107
|
+
if (repoEntries.length > 0) {
|
|
1108
|
+
const repoLines = repoEntries.map(([id, repo]) => `- ${id}: ${repo.path || id}`);
|
|
1109
|
+
parts.push(`Repos in coordinator scope:\n${repoLines.join('\n')}`);
|
|
1110
|
+
}
|
|
1111
|
+
const wsEntries = Object.entries(coordinatorConfig.workstreams || {});
|
|
1112
|
+
if (wsEntries.length > 0) {
|
|
1113
|
+
const wsLines = wsEntries.map(([id, ws]) => `- ${id} (phase: ${ws.phase}, repos: ${(ws.repos || []).join(', ')})`);
|
|
1114
|
+
parts.push(`Coordinator workstreams (reference — plan workstreams may differ):\n${wsLines.join('\n')}`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
614
1118
|
const userPrompt = parts.join('\n\n');
|
|
615
1119
|
return { systemPrompt, userPrompt };
|
|
616
1120
|
}
|