Skip to content

Commit d42fbc4

Browse files
authored
Merge pull request #241 from arul28/ade/queue-pr-fixes-9c471080
queue pr fixes
2 parents 9bb87c8 + fbdf407 commit d42fbc4

38 files changed

Lines changed: 5006 additions & 448 deletions

apps/ade-cli/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ ade git user-identity --lane lane-id --text
6262
ade prs create --lane lane-id --base main --title "Fix checkout flow"
6363
ade prs list-open --text
6464
ade prs path-to-merge --pr pr-id --model gpt-5.5 --max-rounds 3 --no-auto-merge
65+
ade prs path-to-merge --pr pr-id --model gpt-5.5 --conflict-strategy auto --force-finalize conditional
66+
ade prs pipeline pr-id save --conflict-strategy rebase --no-early-merge-on-green
6567
ade run defs --text
6668
ade run start web --lane lane-id
6769
ade shell start --lane lane-id -- npm test
@@ -81,6 +83,25 @@ ade cursor cloud me
8183

8284
Use typed commands first. They validate common arguments and provide stable JSON fields or readable text summaries. Use `ade help <command> <subcommand>` for exact flags, `ade actions list --text` to discover the full service-backed action catalog, and `ade actions run <domain.action>` only when there is no typed command for the workflow yet.
8385

86+
The `prs path-to-merge` and `prs pipeline save` commands persist a partial `PipelineSettings` patch via `issue_inventory.savePipelineSettings` before launching the resolver. The Path to Merge orchestrator reads these from saved settings, so the same flags work either way:
87+
88+
| Flag | PipelineSettings field | Values |
89+
| --- | --- | --- |
90+
| `--max-rounds <n>` (alias `--rounds`) | `maxRounds` | positive integer |
91+
| `--auto-merge` / `--no-auto-merge` | `autoMerge` | boolean |
92+
| `--merge-method <m>` | `mergeMethod` | `repo_default` \| `merge` \| `squash` \| `rebase` |
93+
| `--conflict-strategy <s>` | `conflictStrategy` | `pause` \| `rebase` \| `merge` \| `auto` |
94+
| `--force-finalize <m>` | `forceFinalizeMode` | `off` \| `conditional` \| `unconditional` |
95+
| `--force-finalize-require-no-ci` / `--force-finalize-allow-ci` | `forceFinalizeRequireNoCiFailures` | boolean |
96+
| `--early-merge-on-green` / `--no-early-merge-on-green` | `earlyMergeOnGreen` | boolean |
97+
98+
To set fields without a dedicated flag (for example `autoAgentSettings`), call the action directly:
99+
100+
```bash
101+
ade actions run issue_inventory.savePipelineSettings --args-list-json \
102+
'["pr-1",{"autoAgentSettings":{"provider":"claude","model":"sonnet","reasoningEffort":"high","permissionMode":"guarded_edit","confidenceThreshold":0.7}}]'
103+
```
104+
84105
Output modes are explicit:
85106

86107
```bash

apps/ade-cli/src/cli.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,72 @@ describe("ADE CLI", () => {
233233
});
234234
});
235235

236+
it("forwards new pipeline-settings flags through Path to Merge", () => {
237+
const plan = buildCliPlan([
238+
"prs",
239+
"path-to-merge",
240+
"pr-2",
241+
"--model",
242+
"gpt-5.4",
243+
"--conflict-strategy",
244+
"auto",
245+
"--force-finalize",
246+
"conditional",
247+
"--no-early-merge-on-green",
248+
]);
249+
expect(plan.kind).toBe("execute");
250+
if (plan.kind !== "execute") return;
251+
expect(plan.steps).toHaveLength(2);
252+
expect(plan.steps[0]?.params).toEqual({
253+
name: "run_ade_action",
254+
arguments: {
255+
domain: "issue_inventory",
256+
action: "savePipelineSettings",
257+
argsList: [
258+
"pr-2",
259+
{
260+
conflictStrategy: "auto",
261+
forceFinalizeMode: "conditional",
262+
earlyMergeOnGreen: false,
263+
},
264+
],
265+
},
266+
});
267+
expect(plan.steps[1]?.params).toEqual({
268+
name: "pr_start_issue_resolution",
269+
arguments: {
270+
prId: "pr-2",
271+
scope: "both",
272+
modelId: "gpt-5.4",
273+
},
274+
});
275+
});
276+
277+
it("rejects invalid pipeline-settings enum values", () => {
278+
expect(() =>
279+
buildCliPlan([
280+
"prs",
281+
"path-to-merge",
282+
"pr-3",
283+
"--model",
284+
"gpt-5.4",
285+
"--conflict-strategy",
286+
"wat",
287+
]),
288+
).toThrow(/--conflict-strategy must be one of/);
289+
expect(() =>
290+
buildCliPlan([
291+
"prs",
292+
"path-to-merge",
293+
"pr-3",
294+
"--model",
295+
"gpt-5.4",
296+
"--force-finalize",
297+
"always",
298+
]),
299+
).toThrow(/--force-finalize must be one of/);
300+
});
301+
236302
it("validates required arguments before service execution", () => {
237303
expect(() => buildCliPlan(["lanes", "create"])).toThrow(/name is required/);
238304
expect(() => buildCliPlan(["lanes", "child", "--name", "child"])).toThrow(/parent lane is required/);

apps/ade-cli/src/cli.ts

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,9 @@ const HELP_BY_COMMAND: Record<string, string> = {
725725
$ ade prs comments <pr> --text Show unresolved review work
726726
$ ade prs inventory <pr> Refresh ADE issue inventory
727727
$ ade prs path-to-merge <pr> --model <model> --max-rounds 3 --no-auto-merge
728+
$ ade prs path-to-merge <pr> --model <model> --conflict-strategy auto --force-finalize conditional
729+
$ ade prs path-to-merge <pr> --model <model> --no-early-merge-on-green
730+
$ ade prs pipeline <pr> save --conflict-strategy rebase --early-merge-on-green
728731
$ ade prs resolve-thread <pr> --thread <id> Resolve a review thread
729732
$ ade prs labels set <pr> ready-to-merge Replace labels
730733
$ ade prs reviewers request <pr> alice bob Request reviewers
@@ -1334,6 +1337,72 @@ function maybePut(target: JsonObject, key: string, value: unknown): void {
13341337
}
13351338
}
13361339
1340+
/**
1341+
* Parse the PR pipeline-settings flags shared by `prs path-to-merge` and
1342+
* `prs pipeline` subcommands. Returns a partial `PipelineSettings` patch
1343+
* suitable for `issue_inventory.savePipelineSettings`. Only fields the user
1344+
* explicitly passed are included so the patch never clobbers other settings.
1345+
*
1346+
* The orchestrator reads these from saved settings (not StartPathToMergeArgs),
1347+
* so the path-to-merge command must save them before launching the loop.
1348+
*/
1349+
function readPipelineSettingsPatch(args: string[]): JsonObject {
1350+
const patch: JsonObject = {};
1351+
1352+
const maxRounds = readIntOption(args, ["--max-rounds", "--rounds"]);
1353+
if (maxRounds != null) patch.maxRounds = maxRounds;
1354+
1355+
const autoMerge = readFlag(args, ["--auto-merge"]);
1356+
const noAutoMerge = readFlag(args, ["--no-auto-merge"]);
1357+
if (autoMerge || noAutoMerge) patch.autoMerge = autoMerge && !noAutoMerge;
1358+
1359+
const mergeMethod = readValue(args, ["--merge-method"]);
1360+
if (mergeMethod) patch.mergeMethod = mergeMethod;
1361+
1362+
const conflictStrategy = readValue(args, ["--conflict-strategy"]);
1363+
if (conflictStrategy) {
1364+
if (
1365+
conflictStrategy !== "pause"
1366+
&& conflictStrategy !== "rebase"
1367+
&& conflictStrategy !== "merge"
1368+
&& conflictStrategy !== "auto"
1369+
) {
1370+
throw new CliUsageError(
1371+
"--conflict-strategy must be one of pause, rebase, merge, or auto.",
1372+
);
1373+
}
1374+
patch.conflictStrategy = conflictStrategy;
1375+
}
1376+
1377+
const forceFinalize = readValue(args, ["--force-finalize"]);
1378+
if (forceFinalize) {
1379+
if (
1380+
forceFinalize !== "off"
1381+
&& forceFinalize !== "conditional"
1382+
&& forceFinalize !== "unconditional"
1383+
) {
1384+
throw new CliUsageError(
1385+
"--force-finalize must be one of off, conditional, or unconditional.",
1386+
);
1387+
}
1388+
patch.forceFinalizeMode = forceFinalize;
1389+
}
1390+
1391+
const requireNoCi = readFlag(args, ["--force-finalize-require-no-ci"]);
1392+
const allowCi = readFlag(args, ["--force-finalize-allow-ci"]);
1393+
if (requireNoCi || allowCi) {
1394+
patch.forceFinalizeRequireNoCiFailures = requireNoCi && !allowCi;
1395+
}
1396+
1397+
const earlyMergeOn = readFlag(args, ["--early-merge-on-green"]);
1398+
const earlyMergeOff = readFlag(args, ["--no-early-merge-on-green"]);
1399+
if (earlyMergeOn || earlyMergeOff) {
1400+
patch.earlyMergeOnGreen = earlyMergeOn && !earlyMergeOff;
1401+
}
1402+
1403+
return patch;
1404+
}
1405+
13371406
function parseCliArgs(argv: string[]): ParsedCli {
13381407
const command: string[] = [];
13391408
const options: GlobalOptions = {
@@ -1907,19 +1976,16 @@ function buildPrPlan(args: string[]): CliPlan {
19071976
maybePut(input, "reasoning", readValue(args, ["--reasoning"]));
19081977
maybePut(input, "permissionMode", readValue(args, ["--permission-mode", "--permissions"]));
19091978
maybePut(input, "additionalInstructions", readValue(args, ["--instructions", "--additional-instructions"]));
1910-
const maxRounds = readIntOption(args, ["--max-rounds", "--rounds"]);
1911-
const autoMerge = readFlag(args, ["--auto-merge"]);
1912-
const noAutoMerge = readFlag(args, ["--no-auto-merge"]);
1913-
const mergeMethod = readValue(args, ["--merge-method"]);
1979+
// Path to Merge orchestrator reads conflictStrategy / forceFinalizeMode /
1980+
// earlyMergeOnGreen / autoMerge / maxRounds / mergeMethod from saved
1981+
// PipelineSettings, not from the launch args. Persist any user-supplied
1982+
// overrides before the resolver step so the loop picks them up.
1983+
const pipelinePatch = readPipelineSettingsPatch(args);
19141984
const steps: InvocationStep[] = [];
1915-
if (maxRounds != null || autoMerge || noAutoMerge || mergeMethod) {
1985+
if (Object.keys(pipelinePatch).length > 0) {
19161986
steps.push(actionArgsListStep("pipelineSettings", "issue_inventory", "savePipelineSettings", [
19171987
id,
1918-
{
1919-
...(maxRounds != null ? { maxRounds } : {}),
1920-
...(autoMerge || noAutoMerge ? { autoMerge: autoMerge && !noAutoMerge } : {}),
1921-
...(mergeMethod ? { mergeMethod } : {}),
1922-
},
1988+
pipelinePatch,
19231989
]));
19241990
}
19251991
steps.push(actionCallStep("result", mode === "preview" ? "pr_preview_issue_resolution_prompt" : "pr_start_issue_resolution", collectGenericObjectArgs(args, input)));
@@ -1931,12 +1997,7 @@ function buildPrPlan(args: string[]): CliPlan {
19311997
const id = requireValue(prId ?? firstPositional(args), "prId");
19321998
if (mode === "get") return { kind: "execute", label: "PR pipeline", steps: [actionArgsListStep("result", "issue_inventory", "getPipelineSettings", [id])] };
19331999
if (mode === "delete") return { kind: "execute", label: "PR pipeline delete", steps: [actionArgsListStep("result", "issue_inventory", "deletePipelineSettings", [id])] };
1934-
const maxRounds = readIntOption(args, ["--max-rounds", "--rounds"]);
1935-
const mergeMethod = readValue(args, ["--merge-method"]);
1936-
const settings = collectGenericObjectArgs(args, {
1937-
...(maxRounds != null ? { maxRounds } : {}),
1938-
...(mergeMethod ? { mergeMethod } : {}),
1939-
});
2000+
const settings = collectGenericObjectArgs(args, readPipelineSettingsPatch(args));
19402001
return { kind: "execute", label: "PR pipeline save", steps: [actionArgsListStep("result", "issue_inventory", "savePipelineSettings", [id, settings])] };
19412002
}
19422003

apps/desktop/src/main/main.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { createPrService } from "./services/prs/prService";
4444
import { createPrPollingService } from "./services/prs/prPollingService";
4545
import { createQueueLandingService } from "./services/prs/queueLandingService";
4646
import { createIssueInventoryService } from "./services/prs/issueInventoryService";
47+
import { createPathToMergeOrchestrator } from "./services/prs/pathToMergeOrchestrator";
4748
import { createPrSummaryService } from "./services/prs/prSummaryService";
4849
import {
4950
detectDefaultBaseRef,
@@ -2424,6 +2425,27 @@ app.whenReady().then(async () => {
24242425
// Wire agentChatService into prService for integration resolution
24252426
prService.setAgentChatService(agentChatService);
24262427

2428+
const pathToMergeOrchestrator = createPathToMergeOrchestrator({
2429+
logger,
2430+
prService,
2431+
laneService,
2432+
agentChatService,
2433+
sessionService,
2434+
issueInventoryService,
2435+
conflictService,
2436+
defaultModelId: null,
2437+
defaultReasoningEffort: null,
2438+
});
2439+
setImmediate(() => {
2440+
try {
2441+
pathToMergeOrchestrator.resumeFromPersistedState();
2442+
} catch (err) {
2443+
logger.warn("path_to_merge.resume_failed", {
2444+
error: err instanceof Error ? err.message : String(err),
2445+
});
2446+
}
2447+
});
2448+
24272449
const gitService = createGitOperationsService({
24282450
laneService,
24292451
operationService,
@@ -2925,6 +2947,7 @@ app.whenReady().then(async () => {
29252947
conflictService,
29262948
prService,
29272949
issueInventoryService,
2950+
pathToMergeOrchestrator,
29282951
queueLandingService,
29292952
sessionService,
29302953
ptyService,
@@ -3763,6 +3786,7 @@ app.whenReady().then(async () => {
37633786
appControlService,
37643787
queueLandingService,
37653788
issueInventoryService,
3789+
pathToMergeOrchestrator,
37663790
prSummaryService,
37673791
reviewService,
37683792
jobEngine,
@@ -3874,6 +3898,7 @@ app.whenReady().then(async () => {
38743898
prPollingService: null,
38753899
queueLandingService: null,
38763900
issueInventoryService: null,
3901+
pathToMergeOrchestrator: null,
38773902
prSummaryService: null,
38783903
reviewService: null,
38793904
jobEngine: null,

apps/desktop/src/main/services/ipc/registerIpc.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ import type { createPrService } from "../prs/prService";
587587
import type { createPrPollingService } from "../prs/prPollingService";
588588
import type { createQueueLandingService } from "../prs/queueLandingService";
589589
import type { createIssueInventoryService } from "../prs/issueInventoryService";
590+
import type { PathToMergeOrchestrator } from "../prs/pathToMergeOrchestrator";
590591
import type { createPrSummaryService } from "../prs/prSummaryService";
591592
import type { createReviewService } from "../review/reviewService";
592593
import type { createAgentChatService } from "../chat/agentChatService";
@@ -691,6 +692,7 @@ export type AppContext = {
691692
prPollingService: ReturnType<typeof createPrPollingService>;
692693
queueLandingService: ReturnType<typeof createQueueLandingService>;
693694
issueInventoryService: ReturnType<typeof createIssueInventoryService>;
695+
pathToMergeOrchestrator?: PathToMergeOrchestrator | null;
694696
prSummaryService: ReturnType<typeof createPrSummaryService>;
695697
reviewService: ReturnType<typeof createReviewService>;
696698
jobEngine: ReturnType<typeof createJobEngine>;
@@ -7274,6 +7276,11 @@ export function registerIpc({
72747276
return await ctx.prService.landStack(arg);
72757277
});
72767278

7279+
ipcMain.handle(IPC.prsRetargetBase, async (_event, arg: { prId: string; baseBranch: string }): Promise<void> => {
7280+
const ctx = getCtx();
7281+
return await ctx.prService.retargetBase(arg.prId, arg.baseBranch);
7282+
});
7283+
72777284
ipcMain.handle(IPC.prsOpenInGitHub, async (_event, arg: { prId: string }): Promise<void> => {
72787285
const ctx = getCtx();
72797286
return await ctx.prService.openInGitHub(arg.prId);
@@ -7869,6 +7876,49 @@ export function registerIpc({
78697876
ipcMain.handle(IPC.prsConvergenceStateDelete, (_e, args: { prId: string }): void =>
78707877
getCtx().issueInventoryService.resetConvergenceRuntime(args.prId));
78717878

7879+
ipcMain.handle(
7880+
IPC.prsPathToMergeStart,
7881+
async (_e, args: {
7882+
prId: string;
7883+
modelId?: string | null;
7884+
reasoning?: string | null;
7885+
scope?: "checks" | "comments" | "both";
7886+
additionalInstructions?: string | null;
7887+
}) => {
7888+
const orchestrator = getCtx().pathToMergeOrchestrator;
7889+
if (!orchestrator) {
7890+
throw new Error("Path to Merge orchestrator is not available in this build.");
7891+
}
7892+
const prId = typeof args?.prId === "string" ? args.prId.trim() : "";
7893+
if (!prId) throw new Error("prId is required");
7894+
return await orchestrator.startPathToMerge({
7895+
prId,
7896+
modelId: typeof args?.modelId === "string" ? args.modelId : null,
7897+
reasoning: typeof args?.reasoning === "string" ? args.reasoning : null,
7898+
scope: args?.scope === "checks" || args?.scope === "comments" || args?.scope === "both"
7899+
? args.scope
7900+
: undefined,
7901+
additionalInstructions: typeof args?.additionalInstructions === "string" ? args.additionalInstructions : null,
7902+
});
7903+
},
7904+
);
7905+
7906+
ipcMain.handle(
7907+
IPC.prsPathToMergeStop,
7908+
async (_e, args: { prId: string; reason?: string | null }) => {
7909+
const orchestrator = getCtx().pathToMergeOrchestrator;
7910+
if (!orchestrator) {
7911+
throw new Error("Path to Merge orchestrator is not available in this build.");
7912+
}
7913+
const prId = typeof args?.prId === "string" ? args.prId.trim() : "";
7914+
if (!prId) throw new Error("prId is required");
7915+
return await orchestrator.stopPathToMerge({
7916+
prId,
7917+
reason: typeof args?.reason === "string" ? args.reason : null,
7918+
});
7919+
},
7920+
);
7921+
78727922
ipcMain.handle(IPC.prsPipelineSettingsGet, (_e, args: { prId: string }): PipelineSettings =>
78737923
getCtx().issueInventoryService.getPipelineSettings(args.prId));
78747924
ipcMain.handle(IPC.prsPipelineSettingsSave, (_e, args: { prId: string; settings: Partial<PipelineSettings> }): void =>

0 commit comments

Comments
 (0)