Skip to content

Commit 4d4a576

Browse files
committed
Merge fix-supervisor-loop
1 parent e2ed9d9 commit 4d4a576

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

src/lib/orchestrator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export class Orchestrator {
166166
private toastCallback: ToastCallback | null = null;
167167
private notifyCallback: NotifyCallback | null = null;
168168
private jobsLaunchedCount = 0;
169+
private approvedForMerge = new Set<string>();
169170
private firstJobCompleted = false;
170171

171172
private getMergeTrainConfig(): {
@@ -280,10 +281,20 @@ export class Orchestrator {
280281
`Checkpoint mismatch: expected "${this.checkpoint}", got "${checkpoint}"`,
281282
);
282283
}
284+
const wasPreMerge = this.checkpoint === 'pre_merge';
283285
this.checkpoint = null;
284286

285287
const plan = await loadPlan();
286288
if (plan && plan.status === 'paused') {
289+
// Track jobs approved for merge so reconciler doesn't re-checkpoint them
290+
if (wasPreMerge) {
291+
for (const job of plan.jobs) {
292+
if (job.status === 'ready_to_merge') {
293+
this.approvedForMerge.add(job.name);
294+
}
295+
}
296+
}
297+
287298
plan.status = 'running';
288299
plan.checkpoint = null;
289300
await savePlan(plan);
@@ -483,7 +494,7 @@ export class Orchestrator {
483494
continue;
484495
}
485496

486-
if (this.isSupervisor(plan)) {
497+
if (this.isSupervisor(plan) && !this.approvedForMerge.has(job.name)) {
487498
await this.setCheckpoint('pre_merge', plan);
488499
return;
489500
}
@@ -492,6 +503,7 @@ export class Orchestrator {
492503
this.mergeTrain.enqueue(job);
493504
await updatePlanJob(plan.id, job.name, { status: 'merging' });
494505
job.status = 'merging';
506+
this.approvedForMerge.delete(job.name);
495507
}
496508

497509
if (this.mergeTrain && this.mergeTrain.getQueue().length > 0) {

tests/lib/orchestrator-modes.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,53 @@ describe('orchestrator modes', () => {
388388
expect(orchestrator.clearCheckpoint('pre_pr')).rejects.toThrow('Checkpoint mismatch');
389389
});
390390

391+
it('does not re-checkpoint after pre_merge approval', async () => {
392+
planState = makePlan({
393+
mode: 'supervisor',
394+
status: 'running',
395+
jobs: [
396+
makeJob('merge-me', { status: 'completed', mergeOrder: 0, branch: 'mc/merge-me' }),
397+
],
398+
});
399+
400+
const fakeTrain = {
401+
queue: [] as JobSpec[],
402+
enqueue(job: JobSpec) {
403+
this.queue.push(job);
404+
},
405+
getQueue() {
406+
return [...this.queue];
407+
},
408+
async processNext() {
409+
this.queue.shift();
410+
return { success: true, mergedAt: '2026-01-02T00:00:00.000Z' };
411+
},
412+
};
413+
414+
const orchestrator = new Orchestrator(monitor as any, DEFAULT_CONFIG as any, toastCallback);
415+
416+
// First reconcile: job transitions completed -> ready_to_merge, then supervisor checkpoints
417+
await (orchestrator as any).reconcile();
418+
expect(orchestrator.getCheckpoint()).toBe('pre_merge');
419+
expect(planState?.status).toBe('paused');
420+
421+
// Inject fake merge train before clearing so the auto-reconcile uses it
422+
(orchestrator as any).mergeTrain = fakeTrain;
423+
424+
// Simulate mc_plan_approve clearing the checkpoint
425+
await orchestrator.clearCheckpoint('pre_merge');
426+
expect(orchestrator.getCheckpoint()).toBeNull();
427+
expect(planState?.status).toBe('running');
428+
429+
// Wait for the auto-reconcile triggered by clearCheckpoint/startReconciler
430+
await new Promise((resolve) => setTimeout(resolve, 50));
431+
432+
// Job should have moved to merging (enqueued in merge train) and then merged
433+
expect(orchestrator.getCheckpoint()).not.toBe('pre_merge');
434+
const mergeJob = planState?.jobs.find(j => j.name === 'merge-me');
435+
expect(mergeJob?.status).toBe('merged');
436+
});
437+
391438
it('sends checkpoint toast notifications', async () => {
392439
planState = makePlan({
393440
mode: 'supervisor',

0 commit comments

Comments
 (0)