feat(core,engine): split smart merge strategy into prefer-main / prefer-branch
The single "smart" strategy is now two flavors with the new default flipped to prefer-main. Both share a pre-cascade `git fetch origin <currentBranch>` + best-effort fast-forward so a freshly-pushed sibling commit doesn't get clobbered when the fallback resolves a conflict against a stale base. - "smart-prefer-main" (new default): -X ours fallback. Protects just-merged sibling work from being regressed by a concurrent task branch. - "smart-prefer-branch": -X theirs fallback. Equivalent to legacy "smart". Legacy "smart" / "prefer-main" enum values are accepted and normalized via `normalizeMergeConflictStrategy()` so existing settings.json files migrate seamlessly. The fast-forward step gracefully degrades on fetch failure or divergent local main (logs and continues). Updates settings UI dropdown, test helpers, and adds 5 fetch+ff regression tests + 7 normalize-helper tests. Lint cleanup of two empty catch blocks in scripts/release.mjs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
packages/core/src/__tests__/merge-conflict-strategy.test.ts
Normal file
32
packages/core/src/__tests__/merge-conflict-strategy.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeMergeConflictStrategy } from "../types.js";
|
||||
|
||||
describe("normalizeMergeConflictStrategy", () => {
|
||||
it("maps legacy 'smart' to 'smart-prefer-branch'", () => {
|
||||
expect(normalizeMergeConflictStrategy("smart")).toBe("smart-prefer-branch");
|
||||
});
|
||||
|
||||
it("maps legacy 'prefer-main' to 'smart-prefer-main'", () => {
|
||||
expect(normalizeMergeConflictStrategy("prefer-main")).toBe("smart-prefer-main");
|
||||
});
|
||||
|
||||
it("returns 'smart-prefer-main' as the default when undefined", () => {
|
||||
expect(normalizeMergeConflictStrategy(undefined)).toBe("smart-prefer-main");
|
||||
});
|
||||
|
||||
it("passes through canonical 'smart-prefer-main'", () => {
|
||||
expect(normalizeMergeConflictStrategy("smart-prefer-main")).toBe("smart-prefer-main");
|
||||
});
|
||||
|
||||
it("passes through canonical 'smart-prefer-branch'", () => {
|
||||
expect(normalizeMergeConflictStrategy("smart-prefer-branch")).toBe("smart-prefer-branch");
|
||||
});
|
||||
|
||||
it("passes through 'ai-only' unchanged", () => {
|
||||
expect(normalizeMergeConflictStrategy("ai-only")).toBe("ai-only");
|
||||
});
|
||||
|
||||
it("passes through 'abort' unchanged", () => {
|
||||
expect(normalizeMergeConflictStrategy("abort")).toBe("abort");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, MergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata, normalizeMergeConflictStrategy } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
||||
export {
|
||||
BUILTIN_AGENT_PROMPTS,
|
||||
|
||||
@@ -115,7 +115,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
worktreeRebaseBeforeMerge: true,
|
||||
worktreeRebaseRemote: "",
|
||||
worktreeRebaseLocalBase: true,
|
||||
mergeConflictStrategy: "smart",
|
||||
mergeConflictStrategy: "smart-prefer-main",
|
||||
workflowStepTimeoutMs: 360_000,
|
||||
strictScopeEnforcement: false,
|
||||
buildRetryCount: 0,
|
||||
|
||||
@@ -93,15 +93,57 @@ export type ColorTheme = (typeof COLOR_THEMES)[number];
|
||||
export type PrStatus = "open" | "closed" | "merged";
|
||||
export type MergeStrategy = "direct" | "pull-request";
|
||||
/** How merge conflicts are resolved when the AI agent can't (or shouldn't) decide.
|
||||
* - "smart" (default): try AI, then auto-resolve lock/generated/trivial files,
|
||||
* then fall back to `-X theirs` (task branch wins). Backwards-compatible.
|
||||
*
|
||||
* Both `smart-*` strategies share the same cascade: pre-merge fetch +
|
||||
* fast-forward of local main from origin (graceful degrade on failure),
|
||||
* then AI, then auto-resolve lock/generated/trivial files. They differ only
|
||||
* in the final per-file fallback when conflicts remain:
|
||||
*
|
||||
* - "smart-prefer-main" (default): fall back to `-X ours` so main's state
|
||||
* wins. Best when concurrent tasks could regress just-merged sibling work.
|
||||
* - "smart-prefer-branch": fall back to `-X theirs` so the task branch wins.
|
||||
* Best when one agent at a time is dominant and you trust their output.
|
||||
* - "ai-only": run AI on every attempt; never silently prefer one side.
|
||||
* - "prefer-main": after AI/auto-resolve, fall back to `-X ours` so main's
|
||||
* state wins. Best when concurrent tasks frequently regress just-merged
|
||||
* sibling work.
|
||||
* - "abort": run AI once; if conflict remains, fail the merge so a human
|
||||
* can resolve it. */
|
||||
export type MergeConflictStrategy = "smart" | "ai-only" | "prefer-main" | "abort";
|
||||
* can resolve it.
|
||||
*
|
||||
* Legacy values `"smart"` and `"prefer-main"` are accepted for backwards
|
||||
* compatibility and normalized via {@link normalizeMergeConflictStrategy}.
|
||||
* `"smart"` maps to `"smart-prefer-branch"` (its historical fallback) and
|
||||
* `"prefer-main"` maps to `"smart-prefer-main"`. */
|
||||
export type MergeConflictStrategy =
|
||||
| "smart-prefer-main"
|
||||
| "smart-prefer-branch"
|
||||
| "ai-only"
|
||||
| "abort"
|
||||
/** @deprecated use "smart-prefer-branch" */
|
||||
| "smart"
|
||||
/** @deprecated use "smart-prefer-main" */
|
||||
| "prefer-main";
|
||||
|
||||
/** Canonical (post-migration) values that the merger actually dispatches on. */
|
||||
export type CanonicalMergeConflictStrategy = Exclude<
|
||||
MergeConflictStrategy,
|
||||
"smart" | "prefer-main"
|
||||
>;
|
||||
|
||||
/** Translate legacy `mergeConflictStrategy` values into their canonical form.
|
||||
* Pass-through for already-canonical values; defaults to "smart-prefer-main"
|
||||
* when the input is undefined. */
|
||||
export function normalizeMergeConflictStrategy(
|
||||
value: MergeConflictStrategy | undefined,
|
||||
): CanonicalMergeConflictStrategy {
|
||||
switch (value) {
|
||||
case "smart":
|
||||
return "smart-prefer-branch";
|
||||
case "prefer-main":
|
||||
return "smart-prefer-main";
|
||||
case undefined:
|
||||
return "smart-prefer-main";
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
/** Policy for handling task execution when the selected node is unavailable/unhealthy. */
|
||||
export type UnavailableNodePolicy = "block" | "fallback-local";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user