agentxchain 2.129.0 → 2.130.1

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/src/lib/intake.js CHANGED
@@ -1155,6 +1155,18 @@ export function resolveIntent(root, intentId) {
1155
1155
 
1156
1156
  const { intent, intentPath, dirs } = loadedIntent;
1157
1157
 
1158
+ if (intent.status === 'completed') {
1159
+ return {
1160
+ ok: true,
1161
+ intent,
1162
+ previous_status: 'completed',
1163
+ new_status: 'completed',
1164
+ run_outcome: 'completed',
1165
+ no_change: true,
1166
+ exitCode: 0,
1167
+ };
1168
+ }
1169
+
1158
1170
  if (intent.status !== 'executing' && intent.status !== 'blocked') {
1159
1171
  return {
1160
1172
  ok: false,
@@ -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
- return {
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 systemPrompt = `You are a mission decomposition planner for AgentXchain, a governed multi-agent software delivery system.
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
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { loadAllChainReports, loadChainReport, loadLatestChainReport } from './chain-reports.js';
4
- import { buildPlanProgressSummary, loadLatestPlan } from './mission-plans.js';
4
+ import { buildPlanProgressSummary, loadLatestPlan, synchronizeCoordinatorPlanState } from './mission-plans.js';
5
5
  import { getActiveRepoDecisions } from './repo-decisions.js';
6
6
  import { getCoordinatorStatus } from './coordinator-state.js';
7
7
 
@@ -141,7 +141,7 @@ export function buildMissionSnapshot(root, missionArtifact) {
141
141
  const totalRuns = chains.reduce((sum, chain) => sum + (chain.runs?.length || 0), 0);
142
142
  const totalTurns = chains.reduce((sum, chain) => sum + (chain.total_turns || 0), 0);
143
143
  const latestChain = chains[0] || null;
144
- const latestPlan = loadLatestPlan(root, missionArtifact.mission_id);
144
+ let latestPlan = loadLatestPlan(root, missionArtifact.mission_id);
145
145
  const activeRepoDecisions = getActiveRepoDecisions(root);
146
146
 
147
147
  // Load coordinator status if mission is bound to a multi-repo coordinator
@@ -156,6 +156,13 @@ export function buildMissionSnapshot(root, missionArtifact) {
156
156
  }
157
157
  }
158
158
 
159
+ if (latestPlan && missionArtifact.coordinator && latestPlan.coordinator_scope) {
160
+ const syncedPlan = synchronizeCoordinatorPlanState(root, missionArtifact, latestPlan);
161
+ if (syncedPlan.ok) {
162
+ latestPlan = syncedPlan.plan;
163
+ }
164
+ }
165
+
159
166
  return {
160
167
  ...missionArtifact,
161
168
  derived_status: deriveMissionStatus(missionArtifact, chains, missingChainIds, coordinatorStatus),
@@ -622,6 +622,7 @@ export function checkCleanBaseline(root, writeAuthority) {
622
622
 
623
623
  return {
624
624
  clean: false,
625
+ dirty_files: actorDirtyFiles,
625
626
  reason: `Working tree has uncommitted changes in actor-owned files: ${actorDirtyFiles.slice(0, 5).join(', ')}${actorDirtyFiles.length > 5 ? '...' : ''}. Authoritative/proposed turns require a clean baseline in v1. Commit or stash those changes before assigning the next code-writing turn.`,
626
627
  };
627
628
  }
@@ -20,6 +20,7 @@ export const VALID_RUN_EVENTS = [
20
20
  'turn_conflicted',
21
21
  'acceptance_failed',
22
22
  'turn_reissued',
23
+ 'turn_checkpointed',
23
24
  'run_blocked',
24
25
  'run_completed',
25
26
  'escalation_raised',