Skip to content

Commit fb5a53f

Browse files
committed
feat: store structured checkpoint context on all error checkpoints
Record failureKind and metadata (touchSetViolations, touchSetPatterns) when setting on_error checkpoints. This replaces error-string parsing with structured data for accept/relaunch/retry decision-making. Update touchSet violation notification to present all three options: accept, relaunch (agent fixes), or retry (user fixes, re-validate).
1 parent cbbecd4 commit fb5a53f

1 file changed

Lines changed: 36 additions & 8 deletions

File tree

src/lib/orchestrator.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomUUID } from 'crypto';
22
import type { MCConfig } from './config';
3-
import type { PlanSpec, JobSpec, PlanStatus, CheckpointType } from './plan-types';
3+
import type { PlanSpec, JobSpec, PlanStatus, CheckpointType, CheckpointContext } from './plan-types';
44
import { loadPlan, savePlan, updatePlanJob, clearPlan, validateGhAuth } from './plan-state';
55
import { getDefaultBranch } from './git';
66
import { createIntegrationBranch, deleteIntegrationBranch } from './integration';
@@ -302,6 +302,7 @@ export class Orchestrator {
302302

303303
plan.status = 'running';
304304
plan.checkpoint = null;
305+
plan.checkpointContext = null;
305306
await savePlan(plan);
306307
this.showToast('Mission Control', 'Checkpoint cleared, resuming execution.', 'info');
307308

@@ -388,10 +389,11 @@ export class Orchestrator {
388389
this.isRunning = false;
389390
}
390391

391-
private async setCheckpoint(type: CheckpointType, plan: PlanSpec): Promise<void> {
392+
private async setCheckpoint(type: CheckpointType, plan: PlanSpec, context?: CheckpointContext): Promise<void> {
392393
this.checkpoint = type;
393394
plan.status = 'paused';
394395
plan.checkpoint = type;
396+
plan.checkpointContext = context ?? null;
395397
await savePlan(plan);
396398
this.stopReconciler();
397399
this.showToast(
@@ -501,8 +503,21 @@ export class Orchestrator {
501503
job.status = 'failed';
502504

503505
this.showToast('Mission Control', `Job "${job.name}" touched files outside its touchSet. Plan paused.`, 'error');
504-
this.notify(`❌ Job "${job.name}" modified files outside its touchSet:\n Violations: ${validation.violations.join(', ')}\n Allowed: ${job.touchSet.join(', ')}\nFix the branch and retry with mc_plan_approve(checkpoint: "on_error", retry: "${job.name}").`);
505-
await this.setCheckpoint('on_error', plan);
506+
this.notify(
507+
`❌ Job "${job.name}" modified files outside its touchSet:\n` +
508+
` Violations: ${validation.violations.join(', ')}\n` +
509+
` Allowed: ${job.touchSet.join(', ')}\n\n` +
510+
`Options:\n` +
511+
` Accept violations: mc_plan_approve(checkpoint: "on_error")\n` +
512+
` Agent fixes branch: mc_plan_approve(checkpoint: "on_error", relaunch: "${job.name}")\n` +
513+
` You fix, re-check: mc_plan_approve(checkpoint: "on_error", retry: "${job.name}")`,
514+
);
515+
await this.setCheckpoint('on_error', plan, {
516+
jobName: job.name,
517+
failureKind: 'touchset',
518+
touchSetViolations: validation.violations,
519+
touchSetPatterns: job.touchSet,
520+
});
506521
return;
507522
}
508523
}
@@ -539,7 +554,10 @@ export class Orchestrator {
539554

540555
this.showToast('Mission Control', `Job "${job.name}" has merge conflicts. Plan paused.`, 'error');
541556
this.notify(`❌ Job "${job.name}" would conflict with the integration branch.\n Files: ${mergeCheck.conflicts?.join(', ') ?? 'unknown'}\nRebase the job branch and retry with mc_plan_approve(checkpoint: "on_error", retry: "${job.name}").`);
542-
await this.setCheckpoint('on_error', plan);
557+
await this.setCheckpoint('on_error', plan, {
558+
jobName: job.name,
559+
failureKind: 'merge_conflict',
560+
});
543561
return;
544562
}
545563
}
@@ -590,7 +608,10 @@ export class Orchestrator {
590608

591609
this.showToast('Mission Control', `Merge conflict in job "${nextJob.name}". Plan paused.`, 'error');
592610
this.notify(`❌ Merge conflict in job "${nextJob.name}". Files: ${mergeResult.files?.join(', ') ?? 'unknown'}. Fix the branch and retry with mc_plan_approve(checkpoint: "on_error", retry: "${nextJob.name}").`);
593-
await this.setCheckpoint('on_error', plan);
611+
await this.setCheckpoint('on_error', plan, {
612+
jobName: nextJob.name,
613+
failureKind: 'merge_conflict',
614+
});
594615
return;
595616
} else {
596617
await updatePlanJob(plan.id, nextJob.name, {
@@ -605,7 +626,10 @@ export class Orchestrator {
605626

606627
this.showToast('Mission Control', `Job "${nextJob.name}" failed merge tests. Plan paused.`, 'error');
607628
this.notify(`❌ Job "${nextJob.name}" failed merge tests. Fix the branch and retry with mc_plan_approve(checkpoint: "on_error", retry: "${nextJob.name}").`);
608-
await this.setCheckpoint('on_error', plan);
629+
await this.setCheckpoint('on_error', plan, {
630+
jobName: nextJob.name,
631+
failureKind: 'test_failure',
632+
});
609633
return;
610634
}
611635
}
@@ -893,7 +917,10 @@ If your work needs human review before it can proceed: mc_report(status: "needs_
893917

894918
this.showToast('Mission Control', `Job "${job.name}" failed. Plan paused.`, 'error');
895919
this.notify(`❌ Job "${job.name}" failed. Fix and retry with mc_plan_approve(checkpoint: "on_error", retry: "${job.name}").`);
896-
await this.setCheckpoint('on_error', plan);
920+
await this.setCheckpoint('on_error', plan, {
921+
jobName: job.name,
922+
failureKind: 'job_failed',
923+
});
897924
})
898925
.catch(() => {})
899926
.finally(() => {
@@ -945,6 +972,7 @@ If your work needs human review before it can proceed: mc_report(status: "needs_
945972
if (plan.status === 'paused') {
946973
plan.status = 'running';
947974
plan.checkpoint = null;
975+
plan.checkpointContext = null;
948976
await savePlan(plan);
949977
}
950978
this.checkpoint = null;

0 commit comments

Comments
 (0)