fix(engine): honor per-task auto-merge override when global auto-merge is off
Tasks with autoMerge explicitly enabled never auto-merged when the project-level setting was disabled: the merge enqueue gate (allowInReviewMergeProcessing) and all 19 in-review self-healing sweeps checked only settings.autoMerge, and the board stall-signal hydration passed the raw global into the diagnostic gates. Introduce allowsAutoMergeProcessing(task, settings) in core — additive relative to the global setting so configs with global auto-merge ON are unchanged (explicit autoMerge:false tasks still flow to the merger's manual-required parking) — and use it at the enqueue gate, every self-healing sweep, and the store's stall/stalled signal contexts.
This commit is contained in:
5
.changeset/per-task-automerge-override.md
Normal file
5
.changeset/per-task-automerge-override.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Respect per-task auto-merge overrides when the global auto-merge setting is off. Tasks with auto-merge explicitly enabled now get enqueued for merge and covered by the in-review self-healing sweeps (stall surfacing, merged-task finalization, retry recovery) even when the project-level setting is disabled; tasks without an explicit override keep the PR-based/manual review flow untouched.
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getTaskHardMergeBlocker,
|
||||
getTaskMergeBlocker,
|
||||
isTaskReadyForMerge,
|
||||
allowsAutoMergeProcessing,
|
||||
isSharedBranchGroupMemberIntegration,
|
||||
resolveEffectiveAutoMerge,
|
||||
resolveEffectiveGroupAutoMerge,
|
||||
@@ -46,6 +47,23 @@ describe("resolveEffectiveAutoMerge", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("allowsAutoMergeProcessing", () => {
|
||||
it("lets explicit per-task true through when the global setting is off (FN per-task override)", () => {
|
||||
expect(allowsAutoMergeProcessing({ autoMerge: true }, { autoMerge: false })).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks tasks without an explicit override when the global setting is off", () => {
|
||||
expect(allowsAutoMergeProcessing({ autoMerge: undefined }, { autoMerge: false })).toBe(false);
|
||||
expect(allowsAutoMergeProcessing({ autoMerge: false }, { autoMerge: false })).toBe(false);
|
||||
});
|
||||
|
||||
it("lets everything through when the global setting is on — explicit false still flows so the merger can park it manual-required", () => {
|
||||
expect(allowsAutoMergeProcessing({ autoMerge: undefined }, { autoMerge: true })).toBe(true);
|
||||
expect(allowsAutoMergeProcessing({ autoMerge: true }, { autoMerge: true })).toBe(true);
|
||||
expect(allowsAutoMergeProcessing({ autoMerge: false }, { autoMerge: true })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEffectiveGroupAutoMerge", () => {
|
||||
it("prefers explicit true over global false", () => {
|
||||
expect(resolveEffectiveGroupAutoMerge({ autoMerge: true }, { autoMerge: false })).toBe(true);
|
||||
|
||||
@@ -318,6 +318,7 @@ export {
|
||||
getTaskHardMergeBlocker,
|
||||
getTaskCompletionBlocker,
|
||||
isTaskReadyForMerge,
|
||||
allowsAutoMergeProcessing,
|
||||
isSharedBranchGroupMemberIntegration,
|
||||
resolveEffectiveAutoMerge,
|
||||
resolveEffectiveGroupAutoMerge,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { VALID_TRANSITIONS, DEFAULT_SETTINGS, isGlobalOnlySettingsKey, WORKFLOW_
|
||||
import { DEFAULT_PROJECT_SETTINGS } from "./settings-schema.js";
|
||||
import { resolveWorktrunkSettings, validateWorktrunkSettings } from "./worktrunk-settings.js";
|
||||
import { normalizeTaskPriority } from "./task-priority.js";
|
||||
import { allowsAutoMergeProcessing } from "./task-merge.js";
|
||||
import { canAgentTakeImplementationTaskForExplicitRouting } from "./agent-role-policy.js";
|
||||
import { GlobalSettingsStore } from "./global-settings.js";
|
||||
import { Database, SCHEMA_VERSION, toJson, toJsonNullable, fromJson } from "./db.js";
|
||||
@@ -4597,7 +4598,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const task = this.rowToTask(row);
|
||||
task.inReviewStall = getInReviewStallReason(task, {
|
||||
now,
|
||||
autoMerge: settings.autoMerge,
|
||||
autoMerge: allowsAutoMergeProcessing(task, settings),
|
||||
engineActiveSinceMs: settings.engineActiveSinceMs,
|
||||
engineActivationGraceMs: settings.engineActivationGraceMs,
|
||||
});
|
||||
@@ -4610,7 +4611,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.inReviewStalled = getInReviewStalledSignal(task, {
|
||||
now,
|
||||
thresholdMs: settings.inReviewStalledThresholdMs,
|
||||
autoMerge: settings.autoMerge,
|
||||
autoMerge: allowsAutoMergeProcessing(task, settings),
|
||||
engineActiveSinceMs: settings.engineActiveSinceMs,
|
||||
engineActivationGraceMs: settings.engineActivationGraceMs,
|
||||
});
|
||||
@@ -4853,7 +4854,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const task = this.rowToTask(row);
|
||||
task.inReviewStall = getInReviewStallReason(task, {
|
||||
now,
|
||||
autoMerge: settings.autoMerge,
|
||||
autoMerge: allowsAutoMergeProcessing(task, settings),
|
||||
engineActiveSinceMs: settings.engineActiveSinceMs,
|
||||
engineActivationGraceMs: settings.engineActivationGraceMs,
|
||||
});
|
||||
@@ -4866,7 +4867,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.inReviewStalled = getInReviewStalledSignal(task, {
|
||||
now,
|
||||
thresholdMs: settings.inReviewStalledThresholdMs,
|
||||
autoMerge: settings.autoMerge,
|
||||
autoMerge: allowsAutoMergeProcessing(task, settings),
|
||||
engineActiveSinceMs: settings.engineActiveSinceMs,
|
||||
engineActivationGraceMs: settings.engineActivationGraceMs,
|
||||
});
|
||||
@@ -5016,7 +5017,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const task = this.rowToTask(row);
|
||||
task.inReviewStall = getInReviewStallReason(task, {
|
||||
now,
|
||||
autoMerge: settings.autoMerge,
|
||||
autoMerge: allowsAutoMergeProcessing(task, settings),
|
||||
engineActiveSinceMs: settings.engineActiveSinceMs,
|
||||
engineActivationGraceMs: settings.engineActivationGraceMs,
|
||||
});
|
||||
@@ -5029,7 +5030,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.inReviewStalled = getInReviewStalledSignal(task, {
|
||||
now,
|
||||
thresholdMs: settings.inReviewStalledThresholdMs,
|
||||
autoMerge: settings.autoMerge,
|
||||
autoMerge: allowsAutoMergeProcessing(task, settings),
|
||||
engineActiveSinceMs: settings.engineActiveSinceMs,
|
||||
engineActivationGraceMs: settings.engineActivationGraceMs,
|
||||
});
|
||||
|
||||
@@ -47,6 +47,23 @@ export function resolveEffectiveAutoMerge(
|
||||
return task.autoMerge ?? settings.autoMerge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gate for auto-merge *processing* (engine enqueue + self-healing sweeps).
|
||||
* Additive relative to the global setting: when `settings.autoMerge` is on,
|
||||
* every task flows through — tasks with an explicit `autoMerge: false` are
|
||||
* parked as `manual-required` downstream by the merger, not silently skipped
|
||||
* here. When the global setting is off, only tasks with an explicit per-task
|
||||
* `autoMerge: true` override proceed. Distinct from
|
||||
* `resolveEffectiveAutoMerge`, which resolves the effective boolean and would
|
||||
* (incorrectly for processing gates) starve the manual-required parking path.
|
||||
*/
|
||||
export function allowsAutoMergeProcessing(
|
||||
task: Pick<Task, "autoMerge">,
|
||||
settings: Pick<Settings, "autoMerge">,
|
||||
): boolean {
|
||||
return settings.autoMerge !== false || task.autoMerge === true;
|
||||
}
|
||||
|
||||
// Resolves group → default-branch PROMOTION auto-merge. See resolveEffectiveAutoMerge for the per-task member→group-integration step; the two are distinct and must not be conflated.
|
||||
export function resolveEffectiveGroupAutoMerge(
|
||||
group: Pick<BranchGroup, "autoMerge">,
|
||||
|
||||
@@ -2670,3 +2670,29 @@ describe("ProjectEngine stale mergeActive rescue (FN-3900)", () => {
|
||||
await engine.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("allowInReviewMergeProcessing per-task autoMerge override", () => {
|
||||
const gate = (task: Partial<Task>, settings: { autoMerge: boolean }) =>
|
||||
(createEngine() as any).allowInReviewMergeProcessing(task, settings) as boolean;
|
||||
|
||||
it("lets an explicit per-task autoMerge:true through when the global setting is off", () => {
|
||||
expect(gate({ autoMerge: true }, { autoMerge: false })).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks tasks without a per-task override when the global setting is off", () => {
|
||||
expect(gate({}, { autoMerge: false })).toBe(false);
|
||||
expect(gate({ autoMerge: false }, { autoMerge: false })).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps everything flowing when the global setting is on — explicit autoMerge:false is parked manual-required downstream", () => {
|
||||
expect(gate({}, { autoMerge: true })).toBe(true);
|
||||
expect(gate({ autoMerge: false }, { autoMerge: true })).toBe(true);
|
||||
});
|
||||
|
||||
it("still exempts shared-branch-group member integration when the global setting is off", () => {
|
||||
expect(gate(
|
||||
{ branchContext: { assignmentMode: "shared", groupId: "grp-1" } as Task["branchContext"] },
|
||||
{ autoMerge: false },
|
||||
)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3365,7 +3365,8 @@ describe("SelfHealingManager", () => {
|
||||
const result = await managerWithRecovery.recoverMergeableReviewTasks();
|
||||
|
||||
expect(result).toBe(0);
|
||||
expect(store.listTasks).not.toHaveBeenCalled();
|
||||
// The sweep may list tasks to discover per-task autoMerge overrides,
|
||||
// but must not merge or enqueue anything without one.
|
||||
expect(store.mergeTask).not.toHaveBeenCalled();
|
||||
expect(enqueueMerge).not.toHaveBeenCalled();
|
||||
|
||||
@@ -3747,7 +3748,10 @@ describe("SelfHealingManager", () => {
|
||||
const result = await managerWithRecovery.finalizeNoOpReviewTasks();
|
||||
|
||||
expect(result).toBe(0);
|
||||
expect(store.listTasks).not.toHaveBeenCalled();
|
||||
// The sweep may list tasks to discover per-task autoMerge overrides,
|
||||
// but must not finalize anything without one.
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(store.updateTask).not.toHaveBeenCalled();
|
||||
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
@@ -8227,26 +8231,98 @@ describe("autoMerge gating for mutating in-review sweeps (FN-5147)", () => {
|
||||
"recoverMissingWorktreeReviewFailures",
|
||||
"recoverPartialProgressNoTaskDoneFailures",
|
||||
"reclaimSelfOwnedBranchConflicts",
|
||||
] as const)("skips entirely when autoMerge is disabled (respects PR-based review flow): %s", async (methodName) => {
|
||||
] as const)("performs no mutations when autoMerge is disabled and no per-task override exists: %s", async (methodName) => {
|
||||
if (methodName === "recoverReviewTasksWithFailedPreMergeSteps") {
|
||||
manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project", recoverFailedPreMergeStep: vi.fn() });
|
||||
}
|
||||
const result = await (manager as any)[methodName]();
|
||||
expect(result).toBe(0);
|
||||
expect(store.listTasks).not.toHaveBeenCalled();
|
||||
// The sweep may list tasks to discover per-task autoMerge overrides,
|
||||
// but must not mutate anything without one (respects PR-based review flow).
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(store.updateTask).not.toHaveBeenCalled();
|
||||
expect(store.logEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips entirely when autoMerge is disabled (respects PR-based review flow): recoverCompletionHandoffLimbo", async () => {
|
||||
it("performs no mutations when autoMerge is disabled and no per-task override exists: recoverCompletionHandoffLimbo", async () => {
|
||||
const result = await manager.recoverCompletionHandoffLimbo();
|
||||
expect(result).toBeUndefined();
|
||||
expect(store.listTasks).not.toHaveBeenCalled();
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(store.updateTask).not.toHaveBeenCalled();
|
||||
expect(store.logEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces in-review stalls for tasks with an explicit autoMerge:true override when the global setting is off", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2026-01-01T00:10:00.000Z"));
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
autoMerge: false,
|
||||
globalPause: false,
|
||||
enginePaused: false,
|
||||
taskStuckTimeoutMs: 60_000,
|
||||
});
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
{
|
||||
id: "FN-OVERRIDE",
|
||||
column: "in-review",
|
||||
paused: false,
|
||||
status: "merging",
|
||||
autoMerge: true,
|
||||
steps: [],
|
||||
log: [],
|
||||
updatedAt: new Date(Date.parse("2026-01-01T00:10:00.000Z") - 600_000).toISOString(),
|
||||
columnMovedAt: new Date(Date.parse("2026-01-01T00:10:00.000Z") - 600_000).toISOString(),
|
||||
},
|
||||
]);
|
||||
|
||||
const surfaced = await manager.surfaceInReviewStalls();
|
||||
|
||||
expect(surfaced).toBe(1);
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-OVERRIDE",
|
||||
expect.stringContaining("In-review stall surfaced ["),
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps skipping override-less siblings while processing the override task", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2026-01-01T00:10:00.000Z"));
|
||||
const staleFields = {
|
||||
column: "in-review",
|
||||
paused: false,
|
||||
status: "merging",
|
||||
steps: [],
|
||||
log: [],
|
||||
updatedAt: new Date(Date.parse("2026-01-01T00:10:00.000Z") - 600_000).toISOString(),
|
||||
columnMovedAt: new Date(Date.parse("2026-01-01T00:10:00.000Z") - 600_000).toISOString(),
|
||||
};
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
autoMerge: false,
|
||||
globalPause: false,
|
||||
enginePaused: false,
|
||||
taskStuckTimeoutMs: 60_000,
|
||||
});
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
{ id: "FN-OVERRIDE", autoMerge: true, ...staleFields },
|
||||
{ id: "FN-MANUAL", ...staleFields },
|
||||
]);
|
||||
|
||||
const surfaced = await manager.surfaceInReviewStalls();
|
||||
|
||||
expect(surfaced).toBe(1);
|
||||
expect(store.logEntry).not.toHaveBeenCalledWith(
|
||||
"FN-MANUAL",
|
||||
expect.stringContaining("In-review stall surfaced ["),
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("FN-5335 triple-proof no-action unit coverage", () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
ScheduledTask,
|
||||
AutomationRunResult,
|
||||
} from "@fusion/core";
|
||||
import { compareTasksByPriorityThenAgeAndId, getTaskHardMergeBlocker, isSharedBranchGroupMemberIntegration, normalizeMergerMode, sortTasksByPriorityThenAgeAndId } from "@fusion/core";
|
||||
import { allowsAutoMergeProcessing, compareTasksByPriorityThenAgeAndId, getTaskHardMergeBlocker, isSharedBranchGroupMemberIntegration, normalizeMergerMode, sortTasksByPriorityThenAgeAndId } from "@fusion/core";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { InProcessRuntime } from "./runtimes/in-process-runtime.js";
|
||||
@@ -1383,8 +1383,8 @@ export class ProjectEngine {
|
||||
* pushed wins. listTasks returns createdAt ASC — without this sort an
|
||||
* older low-priority task would start before a later urgent one.
|
||||
*/
|
||||
private allowInReviewMergeProcessing(task: Pick<Task, "branchContext">, settings: Pick<Settings, "autoMerge">): boolean {
|
||||
return settings.autoMerge || isSharedBranchGroupMemberIntegration(task);
|
||||
private allowInReviewMergeProcessing(task: Pick<Task, "branchContext" | "autoMerge">, settings: Pick<Settings, "autoMerge">): boolean {
|
||||
return allowsAutoMergeProcessing(task, settings) || isSharedBranchGroupMemberIntegration(task);
|
||||
}
|
||||
|
||||
private enqueueEligibleInReviewTasks(tasks: readonly Task[], settings: Pick<Settings, "autoMerge">): number {
|
||||
|
||||
@@ -28,7 +28,7 @@ import { promisify } from "node:util";
|
||||
import { setImmediate as setImmediateCb } from "node:timers";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { isAbsolute, join, relative, resolve } from "node:path";
|
||||
import { IN_REVIEW_STALL_DEADLOCK_LOG_PREFIX, IN_REVIEW_STALL_LOG_PREFIX, countRecentIdenticalStallEntries, detectDependencyCycle, detectSelfDefeatingDependency, getInReviewStalledSignal, getInReviewStallReason, getPrimaryPrInfo, getStalePausedReviewSignal, getStalePausedTodoSignal, getTaskHardMergeBlocker, getTaskMergeBlocker, isEphemeralAgent, isMergeRequestContractShadowEnabled, isSharedBranchGroupMemberIntegration, parseExplicitDuplicateMarker, type AgentStore, type ChatStore, type MessageStore, type TaskStore, type Settings, type Task, type MergeDetails, type TaskPriority, type MergeResult } from "@fusion/core";
|
||||
import { IN_REVIEW_STALL_DEADLOCK_LOG_PREFIX, IN_REVIEW_STALL_LOG_PREFIX, allowsAutoMergeProcessing, countRecentIdenticalStallEntries, detectDependencyCycle, detectSelfDefeatingDependency, getInReviewStalledSignal, getInReviewStallReason, getPrimaryPrInfo, getStalePausedReviewSignal, getStalePausedTodoSignal, getTaskHardMergeBlocker, getTaskMergeBlocker, isEphemeralAgent, isMergeRequestContractShadowEnabled, isSharedBranchGroupMemberIntegration, parseExplicitDuplicateMarker, type AgentStore, type ChatStore, type MessageStore, type TaskStore, type Settings, type Task, type MergeDetails, type TaskPriority, type MergeResult } from "@fusion/core";
|
||||
import type { MeshLeaseManager } from "./mesh-lease-manager.js";
|
||||
import { createLogger, schedulerLog } from "./logger.js";
|
||||
import { RemovalReason, classifyTaskWorktree, getRegisteredWorktreeBranchMap, getRegisteredWorktreePaths, isUsableTaskWorktree, removeWorktree, resolveWorktreeBackend, scanIdleWorktrees, scanOrphanedBranches } from "./worktree-pool.js";
|
||||
@@ -2302,14 +2302,14 @@ export class SelfHealingManager {
|
||||
* Backward lifecycle move gated on triple proof (FN-5335).
|
||||
* When the predicate fails, emits `task:reclaim-self-owned-branch-conflict-no-action` and skips lifecycle mutation.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async reclaimSelfOwnedBranchConflicts(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const todoCandidates = await this.store.listTasks({ column: "todo", slim: true });
|
||||
const inProgressCandidates = await this.store.listTasks({ column: "in-progress", slim: true });
|
||||
const inProgressByWorktree = new Map<string, string>();
|
||||
@@ -2320,7 +2320,8 @@ export class SelfHealingManager {
|
||||
}
|
||||
const inReviewPausedCandidates = (await this.store.listTasks({ column: "in-review", slim: true }))
|
||||
.filter((task) => task.paused === true && task.pausedReason === "branch-conflict-unrecoverable");
|
||||
const candidates = [...todoCandidates, ...inProgressCandidates, ...inReviewPausedCandidates];
|
||||
const candidates = [...todoCandidates, ...inProgressCandidates, ...inReviewPausedCandidates]
|
||||
.filter((task) => allowsAutoMergeProcessing(task, settings));
|
||||
|
||||
const activeTaskIds = new Set<string>();
|
||||
if (this.options.agentStore) {
|
||||
@@ -4484,17 +4485,18 @@ export class SelfHealingManager {
|
||||
* Backward lifecycle move gated on triple proof (FN-5335).
|
||||
* When the unproven fallback predicate fails, emits `task:finalize-no-op-review-no-action` and skips lifecycle mutation.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async finalizeNoOpReviewTasks(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const candidates = tasks.filter((t) =>
|
||||
t.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(t, settings) &&
|
||||
!t.paused &&
|
||||
!isSharedBranchGroupMemberIntegration(t) &&
|
||||
Boolean(t.worktree) &&
|
||||
@@ -4793,12 +4795,11 @@ export class SelfHealingManager {
|
||||
// "pull-request"`) — see GitHub issue #21.
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
|
||||
const mergeable = tasks.filter((t) =>
|
||||
t.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(t, settings) &&
|
||||
!t.paused &&
|
||||
t.status !== "failed" &&
|
||||
// Exclude transient merge statuses. Active merges should be left alone;
|
||||
@@ -4898,7 +4899,9 @@ export class SelfHealingManager {
|
||||
* per-task `postReviewFixCount` so a persistently-failing verifier cannot
|
||||
* ping-pong a task forever.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
* @returns Number of tasks sent back for fix
|
||||
*/
|
||||
async recoverReviewTasksWithFailedPreMergeSteps(): Promise<number> {
|
||||
@@ -4908,7 +4911,6 @@ export class SelfHealingManager {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
const maxFixes = settings.maxPostReviewFixes ?? 1;
|
||||
if (!Number.isFinite(maxFixes) || maxFixes <= 0) return 0;
|
||||
|
||||
@@ -4917,6 +4919,7 @@ export class SelfHealingManager {
|
||||
|
||||
const candidates = tasks.filter((task) => {
|
||||
if (task.column !== "in-review") return false;
|
||||
if (!allowsAutoMergeProcessing(task, settings)) return false;
|
||||
if (task.paused) return false;
|
||||
// Preserve terminal/human-handoff statuses (failed, awaiting-user-review,
|
||||
// merging, etc.). Only revive tasks that are otherwise idle.
|
||||
@@ -4994,13 +4997,14 @@ export class SelfHealingManager {
|
||||
* incomplete step instead of leaving the task stranded in review.
|
||||
* Backward lifecycle move gated on triple proof (FN-5335).
|
||||
* When the predicate fails, emits `task:stale-incomplete-review-no-action` and skips lifecycle mutation.
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverStaleIncompleteReviewTasks(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
const timeoutMs = settings.taskStuckTimeoutMs;
|
||||
if (!timeoutMs || timeoutMs <= 0) return 0;
|
||||
|
||||
@@ -5008,6 +5012,7 @@ export class SelfHealingManager {
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const staleIncomplete = tasks.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
!task.paused &&
|
||||
!task.status &&
|
||||
task.steps.length > 0 &&
|
||||
@@ -5056,8 +5061,9 @@ export class SelfHealingManager {
|
||||
* Final-fallback recovery for `in-review` tasks that fell through every other
|
||||
* scan and have sat untouched longer than `taskStuckTimeoutMs`.
|
||||
*
|
||||
* When `settings.autoMerge` is disabled, this sweep is a no-op because
|
||||
* PR-based manual review intentionally leaves tasks in `in-review`.
|
||||
* Tasks not eligible for auto-merge processing (global `autoMerge` off
|
||||
* without an explicit per-task `autoMerge: true` override) are skipped
|
||||
* because PR-based manual review intentionally leaves them in `in-review`.
|
||||
*
|
||||
* The other review-recovery scans each require a specific shape (failed
|
||||
* pre-merge step, incomplete steps, mergeable + worktree present, confirmed
|
||||
@@ -5078,8 +5084,10 @@ export class SelfHealingManager {
|
||||
* each kick refreshes `updatedAt`, so a task that re-enters review and gets
|
||||
* stuck again can only be kicked once per `taskStuckTimeoutMs` window.
|
||||
*
|
||||
* When `settings.autoMerge === false`, this sweep is a no-op because those
|
||||
* projects intentionally use PR-based/manual in-review ownership.
|
||||
* Tasks not eligible for auto-merge processing (global `autoMerge` off
|
||||
* without an explicit per-task `autoMerge: true` override) are skipped
|
||||
* because those projects intentionally use PR-based/manual in-review
|
||||
* ownership.
|
||||
*
|
||||
* @returns Number of tasks kicked back to todo
|
||||
*/
|
||||
@@ -5087,8 +5095,6 @@ export class SelfHealingManager {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const cycleStartMs = Date.now();
|
||||
const timeoutMs = settings.taskStuckTimeoutMs;
|
||||
if (!timeoutMs || timeoutMs <= 0) return 0;
|
||||
@@ -5100,6 +5106,7 @@ export class SelfHealingManager {
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.deletedAt) continue;
|
||||
if (!allowsAutoMergeProcessing(task, settings)) continue;
|
||||
const signal = getInReviewStallReason(task, {
|
||||
now: cycleStartMs,
|
||||
activeMergeTaskId,
|
||||
@@ -5217,14 +5224,14 @@ export class SelfHealingManager {
|
||||
* - `surfaceStalePausedReviews()` owns paused in-review tasks.
|
||||
* - `surfaceInReviewStalls()` owns reason-driven in-review stalls.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async surfaceInReviewStalled(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const cycleStartMs = Date.now();
|
||||
const thresholdMs = settings.inReviewStalledThresholdMs;
|
||||
if (!thresholdMs || thresholdMs <= 0) return 0;
|
||||
@@ -5236,6 +5243,7 @@ export class SelfHealingManager {
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.deletedAt) continue;
|
||||
if (!allowsAutoMergeProcessing(task, settings)) continue;
|
||||
if (task.paused === true) continue;
|
||||
if (task.id === activeMergeTaskId || executingTaskIds.has(task.id)) continue;
|
||||
|
||||
@@ -5389,13 +5397,14 @@ export class SelfHealingManager {
|
||||
* Backward lifecycle move gated on triple proof (FN-5335).
|
||||
* When the predicate fails, emits `task:ghost-review-no-action` and skips lifecycle mutation.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverGhostReviewTasks(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
const timeoutMs = settings.taskStuckTimeoutMs;
|
||||
if (!timeoutMs || timeoutMs <= 0) return 0;
|
||||
|
||||
@@ -5404,6 +5413,7 @@ export class SelfHealingManager {
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const ghosts = tasks.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
!task.paused &&
|
||||
!executingIds.has(task.id) &&
|
||||
!(task.status && GHOST_REVIEW_PRESERVED_STATUSES.has(task.status)) &&
|
||||
@@ -5465,7 +5475,9 @@ export class SelfHealingManager {
|
||||
* If no landed commit is found, it only clears the stale transient status so
|
||||
* the normal mergeable-review recovery can retry the merge.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
* @returns Number of tasks finalized or unblocked
|
||||
*/
|
||||
/**
|
||||
@@ -5486,8 +5498,9 @@ export class SelfHealingManager {
|
||||
* parked as failed and emit `merger:transient-failure-budget-exhausted`
|
||||
* once for diagnostic visibility.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false`, no `requeueForAutoMerge`
|
||||
* callback is wired, or global/engine pause is active.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without a per-task `autoMerge: true` override). No-op when no
|
||||
* `requeueForAutoMerge` callback is wired or global/engine pause is active.
|
||||
*
|
||||
* @returns Number of tasks recovered
|
||||
*/
|
||||
@@ -5496,12 +5509,12 @@ export class SelfHealingManager {
|
||||
if (!requeue) return 0;
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.autoMerge === false) return 0;
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
|
||||
const slim = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const candidates = slim.filter((t) =>
|
||||
t.column === "in-review"
|
||||
&& allowsAutoMergeProcessing(t, settings)
|
||||
&& t.status === "failed"
|
||||
&& (t.mergeRetries ?? 0) >= MAX_AUTO_MERGE_RETRIES
|
||||
&& typeof t.error === "string"
|
||||
@@ -5642,13 +5655,13 @@ export class SelfHealingManager {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
const timeoutMs = settings.taskStuckTimeoutMs;
|
||||
if (!timeoutMs || timeoutMs <= 0) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const candidates = tasks.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
!task.paused &&
|
||||
Boolean(task.status && ACTIVE_MERGE_STATUSES.has(task.status)) &&
|
||||
this.isPastInterruptedMergeGrace(task, timeoutMs),
|
||||
@@ -5956,20 +5969,21 @@ export class SelfHealingManager {
|
||||
* but a later transition failed or another process moved the task before the
|
||||
* final `in-review` → `done` update completed.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
* @returns Number of tasks recovered
|
||||
*/
|
||||
async recoverMergedReviewTasks(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
|
||||
const mergedButNotDone = tasks.filter((t) =>
|
||||
!t.deletedAt &&
|
||||
t.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(t, settings) &&
|
||||
t.mergeDetails?.mergeConfirmed === true,
|
||||
);
|
||||
|
||||
@@ -6087,14 +6101,14 @@ export class SelfHealingManager {
|
||||
* When the no-landed predicate fails, emits `task:stuck-merge-deadlock-no-action` and skips lifecycle mutation.
|
||||
*/
|
||||
/**
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverStuckMergeDeadlocks(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const now = Date.now();
|
||||
const inReview = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const triage = await this.store.listTasks({ column: "triage", slim: true });
|
||||
@@ -6117,6 +6131,7 @@ export class SelfHealingManager {
|
||||
(dep) => dep.column === "triage" || dep.column === "todo",
|
||||
);
|
||||
return task.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
!task.paused &&
|
||||
task.status === "failed" &&
|
||||
(task.mergeRetries ?? 0) >= MAX_AUTO_MERGE_RETRIES &&
|
||||
@@ -6278,18 +6293,19 @@ export class SelfHealingManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverOrphanOnlyScopeViolations(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const candidates = tasks.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
task.status === "failed" &&
|
||||
task.scopeOverride !== true &&
|
||||
task.mergeDetails?.mergeConfirmed !== true &&
|
||||
@@ -6442,19 +6458,20 @@ export class SelfHealingManager {
|
||||
*
|
||||
* Idempotency: recovered tasks are moved to `done`, status/error are cleared,
|
||||
* and mergeRetries reset to 0, so subsequent sweeps will not match them.
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverAlreadyMergedReviewTasks(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const candidates = tasks.filter((task) =>
|
||||
!task.deletedAt &&
|
||||
task.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
task.status === "failed" &&
|
||||
(task.mergeRetries ?? 0) >= MAX_AUTO_MERGE_RETRIES &&
|
||||
task.mergeDetails?.mergeConfirmed !== true &&
|
||||
@@ -6591,19 +6608,20 @@ export class SelfHealingManager {
|
||||
* Recover completed in-review tasks wedged as failed only because a post-done
|
||||
* session continuation hit a non-continuable signature.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverPostDoneNonContinuableWedge(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: false });
|
||||
let recovered = 0;
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.column !== "in-review" || task.deletedAt) continue;
|
||||
if (!allowsAutoMergeProcessing(task, settings)) continue;
|
||||
if (task.paused || task.userPaused) continue;
|
||||
if (task.status !== "failed") continue;
|
||||
if (this.options.isTaskActive?.(task.id)) continue;
|
||||
@@ -6664,18 +6682,19 @@ export class SelfHealingManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverCompletionHandoffLimbo(): Promise<void> {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return;
|
||||
if (settings.autoMerge === false) return;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: false });
|
||||
const now = Date.now();
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.column !== "in-review" || task.paused) continue;
|
||||
if (!allowsAutoMergeProcessing(task, settings)) continue;
|
||||
if (task.status != null || task.mergeDetails != null || task.review != null || task.reviewState != null) continue;
|
||||
if (this.options.isTaskActive?.(task.id)) continue;
|
||||
if (getTaskMergeBlocker(task) !== undefined) continue;
|
||||
@@ -6889,20 +6908,21 @@ export class SelfHealingManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverForeignOnlyContaminatedInReviewTasks(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
const inReview = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const inProgress = await this.store.listTasks({ column: "in-progress", slim: true });
|
||||
const candidates = [
|
||||
...inReview.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
Boolean(task.branch) &&
|
||||
Boolean(task.worktree) &&
|
||||
task.mergeDetails?.mergeConfirmed !== true &&
|
||||
@@ -6911,6 +6931,7 @@ export class SelfHealingManager {
|
||||
),
|
||||
...inProgress.filter((task) =>
|
||||
task.column === "in-progress" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
task.paused === true &&
|
||||
(task.pausedReason === "branch-cross-contamination" || task.pausedReason === "branch-conflict-unrecoverable") &&
|
||||
Boolean(task.branch) &&
|
||||
@@ -7730,18 +7751,19 @@ export class SelfHealingManager {
|
||||
* `restart-recovery-coordinator.ts`.
|
||||
* We clear stale worktree metadata and failure state, keep step progress and
|
||||
* retry counters, then requeue to todo for a clean retry.
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
*/
|
||||
async recoverMissingWorktreeReviewFailures(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
const candidates = tasks.filter((task) =>
|
||||
isRecoverableMissingWorktreeReviewFailureWithProgress(task)
|
||||
|| isRecoverableMissingWorktreeReviewFailureNoProgress(task),
|
||||
allowsAutoMergeProcessing(task, settings)
|
||||
&& (isRecoverableMissingWorktreeReviewFailureWithProgress(task)
|
||||
|| isRecoverableMissingWorktreeReviewFailureNoProgress(task)),
|
||||
);
|
||||
|
||||
if (candidates.length === 0) return 0;
|
||||
@@ -7813,19 +7835,20 @@ export class SelfHealingManager {
|
||||
* - `recoverNoProgressNoTaskDoneFailures`: `in-progress` with zero progress → clean requeue.
|
||||
* - This one: `in-review` with partial progress → bounded requeue preserving work.
|
||||
*
|
||||
* No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
|
||||
* Skips tasks not eligible for auto-merge processing (global `autoMerge`
|
||||
* off without an explicit per-task `autoMerge: true` override) — PR-based
|
||||
* review flow owns lifecycle until human merge.
|
||||
* @returns Number of tasks requeued for retry
|
||||
*/
|
||||
async recoverPartialProgressNoTaskDoneFailures(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
if (settings.autoMerge === false) return 0;
|
||||
|
||||
const tasks = await this.store.listTasks({ column: "in-review", slim: true });
|
||||
|
||||
const candidates = tasks.filter((task) =>
|
||||
task.column === "in-review" &&
|
||||
allowsAutoMergeProcessing(task, settings) &&
|
||||
task.status === "failed" &&
|
||||
isNoTaskDoneFailure(task) &&
|
||||
!task.paused &&
|
||||
|
||||
Reference in New Issue
Block a user