opencode-mission-control 1.1.1 → 1.2.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/dist/index.js +135 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14045,8 +14045,8 @@ var init_plan_types = __esm(() => {
|
|
|
14045
14045
|
waiting_deps: ["running", "stopped", "canceled"],
|
|
14046
14046
|
running: ["completed", "failed", "stopped", "canceled"],
|
|
14047
14047
|
completed: ["ready_to_merge", "stopped", "canceled"],
|
|
14048
|
-
failed: ["stopped", "canceled"],
|
|
14049
|
-
ready_to_merge: ["merging", "stopped", "canceled"],
|
|
14048
|
+
failed: ["ready_to_merge", "stopped", "canceled"],
|
|
14049
|
+
ready_to_merge: ["merging", "needs_rebase", "stopped", "canceled"],
|
|
14050
14050
|
merging: ["merged", "conflict", "stopped", "canceled"],
|
|
14051
14051
|
merged: ["needs_rebase"],
|
|
14052
14052
|
conflict: ["ready_to_merge", "stopped", "canceled"],
|
|
@@ -28495,6 +28495,57 @@ var init_integration = __esm(() => {
|
|
|
28495
28495
|
// src/lib/merge-train.ts
|
|
28496
28496
|
import { join as join9 } from "path";
|
|
28497
28497
|
import { existsSync, lstatSync, rmSync } from "fs";
|
|
28498
|
+
async function validateTouchSet(jobBranch, baseBranch, touchSet, opts) {
|
|
28499
|
+
if (touchSet.length === 0) {
|
|
28500
|
+
return { valid: true };
|
|
28501
|
+
}
|
|
28502
|
+
const diffResult = await gitCommand(["diff", "--name-only", `${baseBranch}...${jobBranch}`], opts);
|
|
28503
|
+
if (diffResult.exitCode !== 0) {
|
|
28504
|
+
return { valid: false, violations: [`Failed to diff: ${diffResult.stderr}`] };
|
|
28505
|
+
}
|
|
28506
|
+
const changedFiles = diffResult.stdout.split(`
|
|
28507
|
+
`).map((f) => f.trim()).filter(Boolean);
|
|
28508
|
+
if (changedFiles.length === 0) {
|
|
28509
|
+
return { valid: true, changedFiles: [] };
|
|
28510
|
+
}
|
|
28511
|
+
const violations = [];
|
|
28512
|
+
for (const file3 of changedFiles) {
|
|
28513
|
+
const matchesAny = touchSet.some((pattern) => {
|
|
28514
|
+
const glob = new Bun.Glob(pattern);
|
|
28515
|
+
return glob.match(file3);
|
|
28516
|
+
});
|
|
28517
|
+
if (!matchesAny) {
|
|
28518
|
+
violations.push(file3);
|
|
28519
|
+
}
|
|
28520
|
+
}
|
|
28521
|
+
return {
|
|
28522
|
+
valid: violations.length === 0,
|
|
28523
|
+
violations: violations.length > 0 ? violations : undefined,
|
|
28524
|
+
changedFiles
|
|
28525
|
+
};
|
|
28526
|
+
}
|
|
28527
|
+
async function checkMergeability(integrationWorktree, jobBranch) {
|
|
28528
|
+
const testMerge = await gitCommand([
|
|
28529
|
+
"-C",
|
|
28530
|
+
integrationWorktree,
|
|
28531
|
+
"merge",
|
|
28532
|
+
"--no-commit",
|
|
28533
|
+
"--no-ff",
|
|
28534
|
+
jobBranch
|
|
28535
|
+
]);
|
|
28536
|
+
await gitCommand(["-C", integrationWorktree, "merge", "--abort"]).catch(() => {});
|
|
28537
|
+
await gitCommand(["-C", integrationWorktree, "reset", "--hard", "HEAD"]).catch(() => {});
|
|
28538
|
+
await gitCommand(["-C", integrationWorktree, "clean", "-fd"]).catch(() => {});
|
|
28539
|
+
if (testMerge.exitCode !== 0) {
|
|
28540
|
+
const conflicts = extractConflicts([testMerge.stdout, testMerge.stderr].filter(Boolean).join(`
|
|
28541
|
+
`));
|
|
28542
|
+
return {
|
|
28543
|
+
canMerge: false,
|
|
28544
|
+
conflicts: conflicts.length > 0 ? conflicts : undefined
|
|
28545
|
+
};
|
|
28546
|
+
}
|
|
28547
|
+
return { canMerge: true };
|
|
28548
|
+
}
|
|
28498
28549
|
async function rollbackMerge(worktreePath) {
|
|
28499
28550
|
await gitCommand(["-C", worktreePath, "merge", "--abort"]).catch(() => {});
|
|
28500
28551
|
await gitCommand(["-C", worktreePath, "reset", "--hard", "HEAD"]).catch(() => {});
|
|
@@ -29368,6 +29419,23 @@ class Orchestrator {
|
|
|
29368
29419
|
}
|
|
29369
29420
|
for (const job of mergeOrder) {
|
|
29370
29421
|
if (job.status === "completed") {
|
|
29422
|
+
if (job.touchSet && job.touchSet.length > 0 && job.branch && plan.integrationBranch) {
|
|
29423
|
+
const validation = await validateTouchSet(job.branch, plan.integrationBranch, job.touchSet);
|
|
29424
|
+
if (!validation.valid && validation.violations) {
|
|
29425
|
+
await updatePlanJob(plan.id, job.name, {
|
|
29426
|
+
status: "failed",
|
|
29427
|
+
error: `Modified files outside touchSet: ${validation.violations.join(", ")}. Expected only: ${job.touchSet.join(", ")}`
|
|
29428
|
+
});
|
|
29429
|
+
job.status = "failed";
|
|
29430
|
+
this.showToast("Mission Control", `Job "${job.name}" touched files outside its touchSet. Plan paused.`, "error");
|
|
29431
|
+
this.notify(`\u274C Job "${job.name}" modified files outside its touchSet:
|
|
29432
|
+
Violations: ${validation.violations.join(", ")}
|
|
29433
|
+
Allowed: ${job.touchSet.join(", ")}
|
|
29434
|
+
Fix the branch and retry with mc_plan_approve(checkpoint: "on_error", retry: "${job.name}").`);
|
|
29435
|
+
await this.setCheckpoint("on_error", plan);
|
|
29436
|
+
return;
|
|
29437
|
+
}
|
|
29438
|
+
}
|
|
29371
29439
|
await updatePlanJob(plan.id, job.name, { status: "ready_to_merge" });
|
|
29372
29440
|
job.status = "ready_to_merge";
|
|
29373
29441
|
}
|
|
@@ -29384,6 +29452,22 @@ class Orchestrator {
|
|
|
29384
29452
|
if (!canMergeNow) {
|
|
29385
29453
|
continue;
|
|
29386
29454
|
}
|
|
29455
|
+
if (job.branch && plan.integrationWorktree) {
|
|
29456
|
+
const mergeCheck = await checkMergeability(plan.integrationWorktree, job.branch);
|
|
29457
|
+
if (!mergeCheck.canMerge) {
|
|
29458
|
+
await updatePlanJob(plan.id, job.name, {
|
|
29459
|
+
status: "needs_rebase",
|
|
29460
|
+
error: mergeCheck.conflicts?.join(", ") ?? "merge conflict detected in trial merge"
|
|
29461
|
+
});
|
|
29462
|
+
job.status = "needs_rebase";
|
|
29463
|
+
this.showToast("Mission Control", `Job "${job.name}" has merge conflicts. Plan paused.`, "error");
|
|
29464
|
+
this.notify(`\u274C Job "${job.name}" would conflict with the integration branch.
|
|
29465
|
+
Files: ${mergeCheck.conflicts?.join(", ") ?? "unknown"}
|
|
29466
|
+
Rebase the job branch and retry with mc_plan_approve(checkpoint: "on_error", retry: "${job.name}").`);
|
|
29467
|
+
await this.setCheckpoint("on_error", plan);
|
|
29468
|
+
return;
|
|
29469
|
+
}
|
|
29470
|
+
}
|
|
29387
29471
|
if (this.isSupervisor(plan) && !this.approvedForMerge.has(job.name)) {
|
|
29388
29472
|
await this.setCheckpoint("pre_merge", plan);
|
|
29389
29473
|
return;
|
|
@@ -29422,13 +29506,10 @@ class Orchestrator {
|
|
|
29422
29506
|
status: "conflict",
|
|
29423
29507
|
error: mergeResult.files?.join(", ") ?? "merge conflict"
|
|
29424
29508
|
});
|
|
29425
|
-
|
|
29426
|
-
|
|
29427
|
-
|
|
29428
|
-
|
|
29429
|
-
plan.status = "failed";
|
|
29430
|
-
this.showToast("Mission Control", `Merge conflict in job "${nextJob.name}".`, "error");
|
|
29431
|
-
this.notify(`\u274C Merge conflict in job "${nextJob.name}". Files: ${mergeResult.files?.join(", ") ?? "unknown"}. Plan failed.`);
|
|
29509
|
+
this.showToast("Mission Control", `Merge conflict in job "${nextJob.name}". Plan paused.`, "error");
|
|
29510
|
+
this.notify(`\u274C 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}").`);
|
|
29511
|
+
await this.setCheckpoint("on_error", plan);
|
|
29512
|
+
return;
|
|
29432
29513
|
} else {
|
|
29433
29514
|
await updatePlanJob(plan.id, nextJob.name, {
|
|
29434
29515
|
status: "failed",
|
|
@@ -29438,13 +29519,10 @@ class Orchestrator {
|
|
|
29438
29519
|
if (testSummary) {
|
|
29439
29520
|
this.notify(`\uD83E\uDDEA ${nextJob.name}: ${testSummary}`);
|
|
29440
29521
|
}
|
|
29441
|
-
|
|
29442
|
-
|
|
29443
|
-
|
|
29444
|
-
|
|
29445
|
-
plan.status = "failed";
|
|
29446
|
-
this.showToast("Mission Control", `Job "${nextJob.name}" failed during merge.`, "error");
|
|
29447
|
-
this.notify(`\u274C Job "${nextJob.name}" failed merge tests. Plan failed.`);
|
|
29522
|
+
this.showToast("Mission Control", `Job "${nextJob.name}" failed merge tests. Plan paused.`, "error");
|
|
29523
|
+
this.notify(`\u274C Job "${nextJob.name}" failed merge tests. Fix the branch and retry with mc_plan_approve(checkpoint: "on_error", retry: "${nextJob.name}").`);
|
|
29524
|
+
await this.setCheckpoint("on_error", plan);
|
|
29525
|
+
return;
|
|
29448
29526
|
}
|
|
29449
29527
|
}
|
|
29450
29528
|
if (plan.status === "merging" && (!this.mergeTrain || this.mergeTrain.getQueue().length === 0)) {
|
|
@@ -29615,20 +29693,22 @@ IMPORTANT: When you have completed ALL of your work, you MUST commit your change
|
|
|
29615
29693
|
}
|
|
29616
29694
|
}
|
|
29617
29695
|
handleJobComplete = (job) => {
|
|
29618
|
-
if (job.planId
|
|
29619
|
-
|
|
29620
|
-
|
|
29621
|
-
|
|
29622
|
-
|
|
29623
|
-
|
|
29696
|
+
if (!job.planId || !this.activePlanId || job.planId !== this.activePlanId) {
|
|
29697
|
+
return;
|
|
29698
|
+
}
|
|
29699
|
+
if (!this.firstJobCompleted) {
|
|
29700
|
+
this.firstJobCompleted = true;
|
|
29701
|
+
this.showToast("Mission Control", `First job completed: "${job.name}".`, "success");
|
|
29702
|
+
}
|
|
29703
|
+
const planId = job.planId;
|
|
29704
|
+
(async () => {
|
|
29705
|
+
await updatePlanJob(planId, job.name, {
|
|
29624
29706
|
status: "completed"
|
|
29625
|
-
}).catch((error92) => {
|
|
29626
|
-
console.error("Failed to update completed job state:", error92);
|
|
29627
|
-
});
|
|
29628
|
-
this.reconcile().catch((error92) => {
|
|
29629
|
-
console.error("Reconcile after completion failed:", error92);
|
|
29630
29707
|
});
|
|
29631
|
-
|
|
29708
|
+
await this.reconcile();
|
|
29709
|
+
})().catch((error92) => {
|
|
29710
|
+
console.error("Failed to reconcile completed job state:", error92);
|
|
29711
|
+
});
|
|
29632
29712
|
};
|
|
29633
29713
|
handleJobFailed = (job) => {
|
|
29634
29714
|
if (job.planId && this.activePlanId && job.planId === this.activePlanId) {
|
|
@@ -29640,15 +29720,9 @@ IMPORTANT: When you have completed ALL of your work, you MUST commit your change
|
|
|
29640
29720
|
if (!plan || plan.id !== job.planId) {
|
|
29641
29721
|
return;
|
|
29642
29722
|
}
|
|
29643
|
-
|
|
29644
|
-
|
|
29645
|
-
|
|
29646
|
-
}
|
|
29647
|
-
plan.status = "failed";
|
|
29648
|
-
plan.completedAt = new Date().toISOString();
|
|
29649
|
-
await savePlan(plan);
|
|
29650
|
-
this.showToast("Mission Control", `Plan failed: job "${job.name}" failed.`, "error");
|
|
29651
|
-
this.notify(`\u274C Plan failed: job "${job.name}" failed.`);
|
|
29723
|
+
this.showToast("Mission Control", `Job "${job.name}" failed. Plan paused.`, "error");
|
|
29724
|
+
this.notify(`\u274C Job "${job.name}" failed. Fix and retry with mc_plan_approve(checkpoint: "on_error", retry: "${job.name}").`);
|
|
29725
|
+
await this.setCheckpoint("on_error", plan);
|
|
29652
29726
|
}).catch(() => {}).finally(() => {
|
|
29653
29727
|
if (!this.checkpoint) {
|
|
29654
29728
|
this.stopReconciler();
|
|
@@ -29881,17 +29955,37 @@ var init_plan_approve = __esm(() => {
|
|
|
29881
29955
|
init_integration();
|
|
29882
29956
|
init_worktree_setup();
|
|
29883
29957
|
mc_plan_approve = tool({
|
|
29884
|
-
description: "Approve a pending copilot plan
|
|
29958
|
+
description: "Approve a pending copilot plan, clear a supervisor checkpoint, or retry a failed job to continue execution",
|
|
29885
29959
|
args: {
|
|
29886
|
-
checkpoint: tool.schema.enum(["pre_merge", "on_error", "pre_pr"]).optional().describe("Specific checkpoint to clear (for supervisor mode)")
|
|
29960
|
+
checkpoint: tool.schema.enum(["pre_merge", "on_error", "pre_pr"]).optional().describe("Specific checkpoint to clear (for supervisor mode)"),
|
|
29961
|
+
retry: tool.schema.string().optional().describe("Name of a failed, conflict, or needs_rebase job to retry")
|
|
29887
29962
|
},
|
|
29888
29963
|
async execute(args) {
|
|
29889
29964
|
const plan = await loadPlan();
|
|
29890
29965
|
if (!plan) {
|
|
29891
29966
|
throw new Error("No active plan to approve");
|
|
29892
29967
|
}
|
|
29968
|
+
if (args.retry) {
|
|
29969
|
+
const job = plan.jobs.find((j) => j.name === args.retry);
|
|
29970
|
+
if (!job) {
|
|
29971
|
+
throw new Error(`Job "${args.retry}" not found in plan`);
|
|
29972
|
+
}
|
|
29973
|
+
if (job.status !== "failed" && job.status !== "conflict" && job.status !== "needs_rebase") {
|
|
29974
|
+
throw new Error(`Job "${args.retry}" is not in a retryable state (current: ${job.status}). Only failed, conflict, or needs_rebase jobs can be retried.`);
|
|
29975
|
+
}
|
|
29976
|
+
}
|
|
29893
29977
|
if (plan.status === "paused" && plan.checkpoint) {
|
|
29894
29978
|
const checkpoint = args.checkpoint ?? plan.checkpoint;
|
|
29979
|
+
if (args.retry) {
|
|
29980
|
+
const job = plan.jobs.find((j) => j.name === args.retry);
|
|
29981
|
+
if (!job) {
|
|
29982
|
+
throw new Error(`Job "${args.retry}" not found in plan`);
|
|
29983
|
+
}
|
|
29984
|
+
if (job.status !== "failed" && job.status !== "conflict" && job.status !== "needs_rebase") {
|
|
29985
|
+
throw new Error(`Job "${args.retry}" is not in a retryable state (current: ${job.status}). Only failed, conflict, or needs_rebase jobs can be retried.`);
|
|
29986
|
+
}
|
|
29987
|
+
await updatePlanJob(plan.id, args.retry, { status: "ready_to_merge", error: undefined });
|
|
29988
|
+
}
|
|
29895
29989
|
plan.status = "running";
|
|
29896
29990
|
plan.checkpoint = null;
|
|
29897
29991
|
await savePlan(plan);
|
|
@@ -29900,8 +29994,9 @@ var init_plan_approve = __esm(() => {
|
|
|
29900
29994
|
setSharedOrchestrator(orchestrator2);
|
|
29901
29995
|
orchestrator2.setPlanModelSnapshot(getCurrentModel());
|
|
29902
29996
|
await orchestrator2.resumePlan();
|
|
29997
|
+
const retryMsg = args.retry ? ` Job "${args.retry}" reset to ready_to_merge.` : "";
|
|
29903
29998
|
return [
|
|
29904
|
-
`Checkpoint "${checkpoint}" cleared
|
|
29999
|
+
`Checkpoint "${checkpoint}" cleared.${retryMsg} Plan "${plan.name}" resuming.`,
|
|
29905
30000
|
"",
|
|
29906
30001
|
` ID: ${plan.id}`,
|
|
29907
30002
|
` Mode: ${plan.mode}`,
|