Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .claude/scheduled_tasks.lock

This file was deleted.

21 changes: 21 additions & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ ade git user-identity --lane lane-id --text
ade prs create --lane lane-id --base main --title "Fix checkout flow"
ade prs list-open --text
ade prs path-to-merge --pr pr-id --model gpt-5.5 --max-rounds 3 --no-auto-merge
ade prs path-to-merge --pr pr-id --model gpt-5.5 --conflict-strategy auto --force-finalize conditional
ade prs pipeline pr-id save --conflict-strategy rebase --no-early-merge-on-green
ade run defs --text
ade run start web --lane lane-id
ade shell start --lane lane-id -- npm test
Expand All @@ -81,6 +83,25 @@ ade cursor cloud me

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.

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:

| Flag | PipelineSettings field | Values |
| --- | --- | --- |
| `--max-rounds <n>` (alias `--rounds`) | `maxRounds` | positive integer |
| `--auto-merge` / `--no-auto-merge` | `autoMerge` | boolean |
| `--merge-method <m>` | `mergeMethod` | `repo_default` \| `merge` \| `squash` \| `rebase` |
| `--conflict-strategy <s>` | `conflictStrategy` | `pause` \| `rebase` \| `merge` \| `auto` |
| `--force-finalize <m>` | `forceFinalizeMode` | `off` \| `conditional` \| `unconditional` |
| `--force-finalize-require-no-ci` / `--force-finalize-allow-ci` | `forceFinalizeRequireNoCiFailures` | boolean |
| `--early-merge-on-green` / `--no-early-merge-on-green` | `earlyMergeOnGreen` | boolean |

To set fields without a dedicated flag (for example `autoAgentSettings`), call the action directly:

```bash
ade actions run issue_inventory.savePipelineSettings --args-list-json \
'["pr-1",{"autoAgentSettings":{"provider":"claude","model":"sonnet","reasoningEffort":"high","permissionMode":"guarded_edit","confidenceThreshold":0.7}}]'
```

Output modes are explicit:

```bash
Expand Down
9 changes: 6 additions & 3 deletions apps/ade-cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,72 @@ describe("ADE CLI", () => {
});
});

it("forwards new pipeline-settings flags through Path to Merge", () => {
const plan = buildCliPlan([
"prs",
"path-to-merge",
"pr-2",
"--model",
"gpt-5.4",
"--conflict-strategy",
"auto",
"--force-finalize",
"conditional",
"--no-early-merge-on-green",
]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;
expect(plan.steps).toHaveLength(2);
expect(plan.steps[0]?.params).toEqual({
name: "run_ade_action",
arguments: {
domain: "issue_inventory",
action: "savePipelineSettings",
argsList: [
"pr-2",
{
conflictStrategy: "auto",
forceFinalizeMode: "conditional",
earlyMergeOnGreen: false,
},
],
},
});
expect(plan.steps[1]?.params).toEqual({
name: "pr_start_issue_resolution",
arguments: {
prId: "pr-2",
scope: "both",
modelId: "gpt-5.4",
},
});
});

it("rejects invalid pipeline-settings enum values", () => {
expect(() =>
buildCliPlan([
"prs",
"path-to-merge",
"pr-3",
"--model",
"gpt-5.4",
"--conflict-strategy",
"wat",
]),
).toThrow(/--conflict-strategy must be one of/);
expect(() =>
buildCliPlan([
"prs",
"path-to-merge",
"pr-3",
"--model",
"gpt-5.4",
"--force-finalize",
"always",
]),
).toThrow(/--force-finalize must be one of/);
});

it("validates required arguments before service execution", () => {
expect(() => buildCliPlan(["lanes", "create"])).toThrow(/name is required/);
expect(() => buildCliPlan(["lanes", "child", "--name", "child"])).toThrow(/parent lane is required/);
Expand Down
93 changes: 77 additions & 16 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,9 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade prs comments <pr> --text Show unresolved review work
$ ade prs inventory <pr> Refresh ADE issue inventory
$ ade prs path-to-merge <pr> --model <model> --max-rounds 3 --no-auto-merge
$ ade prs path-to-merge <pr> --model <model> --conflict-strategy auto --force-finalize conditional
$ ade prs path-to-merge <pr> --model <model> --no-early-merge-on-green
$ ade prs pipeline <pr> save --conflict-strategy rebase --early-merge-on-green
$ ade prs resolve-thread <pr> --thread <id> Resolve a review thread
$ ade prs labels set <pr> ready-to-merge Replace labels
$ ade prs reviewers request <pr> alice bob Request reviewers
Expand Down Expand Up @@ -1290,6 +1293,72 @@ function maybePut(target: JsonObject, key: string, value: unknown): void {
}
}

/**
* Parse the PR pipeline-settings flags shared by `prs path-to-merge` and
* `prs pipeline` subcommands. Returns a partial `PipelineSettings` patch
* suitable for `issue_inventory.savePipelineSettings`. Only fields the user
* explicitly passed are included so the patch never clobbers other settings.
*
* The orchestrator reads these from saved settings (not StartPathToMergeArgs),
* so the path-to-merge command must save them before launching the loop.
*/
function readPipelineSettingsPatch(args: string[]): JsonObject {
const patch: JsonObject = {};

const maxRounds = readIntOption(args, ["--max-rounds", "--rounds"]);
if (maxRounds != null) patch.maxRounds = maxRounds;

const autoMerge = readFlag(args, ["--auto-merge"]);
const noAutoMerge = readFlag(args, ["--no-auto-merge"]);
if (autoMerge || noAutoMerge) patch.autoMerge = autoMerge && !noAutoMerge;

const mergeMethod = readValue(args, ["--merge-method"]);
if (mergeMethod) patch.mergeMethod = mergeMethod;

const conflictStrategy = readValue(args, ["--conflict-strategy"]);
if (conflictStrategy) {
if (
conflictStrategy !== "pause"
&& conflictStrategy !== "rebase"
&& conflictStrategy !== "merge"
&& conflictStrategy !== "auto"
) {
throw new CliUsageError(
"--conflict-strategy must be one of pause, rebase, merge, or auto.",
);
}
patch.conflictStrategy = conflictStrategy;
}

const forceFinalize = readValue(args, ["--force-finalize"]);
if (forceFinalize) {
if (
forceFinalize !== "off"
&& forceFinalize !== "conditional"
&& forceFinalize !== "unconditional"
) {
throw new CliUsageError(
"--force-finalize must be one of off, conditional, or unconditional.",
);
}
patch.forceFinalizeMode = forceFinalize;
}

const requireNoCi = readFlag(args, ["--force-finalize-require-no-ci"]);
const allowCi = readFlag(args, ["--force-finalize-allow-ci"]);
if (requireNoCi || allowCi) {
patch.forceFinalizeRequireNoCiFailures = requireNoCi && !allowCi;
}

const earlyMergeOn = readFlag(args, ["--early-merge-on-green"]);
const earlyMergeOff = readFlag(args, ["--no-early-merge-on-green"]);
if (earlyMergeOn || earlyMergeOff) {
patch.earlyMergeOnGreen = earlyMergeOn && !earlyMergeOff;
}

return patch;
}

function parseCliArgs(argv: string[]): ParsedCli {
const command: string[] = [];
const options: GlobalOptions = {
Expand Down Expand Up @@ -1863,19 +1932,16 @@ function buildPrPlan(args: string[]): CliPlan {
maybePut(input, "reasoning", readValue(args, ["--reasoning"]));
maybePut(input, "permissionMode", readValue(args, ["--permission-mode", "--permissions"]));
maybePut(input, "additionalInstructions", readValue(args, ["--instructions", "--additional-instructions"]));
const maxRounds = readIntOption(args, ["--max-rounds", "--rounds"]);
const autoMerge = readFlag(args, ["--auto-merge"]);
const noAutoMerge = readFlag(args, ["--no-auto-merge"]);
const mergeMethod = readValue(args, ["--merge-method"]);
// Path to Merge orchestrator reads conflictStrategy / forceFinalizeMode /
// earlyMergeOnGreen / autoMerge / maxRounds / mergeMethod from saved
// PipelineSettings, not from the launch args. Persist any user-supplied
// overrides before the resolver step so the loop picks them up.
const pipelinePatch = readPipelineSettingsPatch(args);
const steps: InvocationStep[] = [];
if (maxRounds != null || autoMerge || noAutoMerge || mergeMethod) {
if (Object.keys(pipelinePatch).length > 0) {
steps.push(actionArgsListStep("pipelineSettings", "issue_inventory", "savePipelineSettings", [
id,
{
...(maxRounds != null ? { maxRounds } : {}),
...(autoMerge || noAutoMerge ? { autoMerge: autoMerge && !noAutoMerge } : {}),
...(mergeMethod ? { mergeMethod } : {}),
},
pipelinePatch,
]));
}
steps.push(actionCallStep("result", mode === "preview" ? "pr_preview_issue_resolution_prompt" : "pr_start_issue_resolution", collectGenericObjectArgs(args, input)));
Comment on lines +1935 to 1947
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't persist pipeline overrides during preview.

This now runs issue_inventory.savePipelineSettings for ade prs path-to-merge preview ... too, so a prompt preview mutates the PR's saved pipeline configuration. That makes a read-only preview stateful and can silently affect later real runs.

Suggested fix
-    if (Object.keys(pipelinePatch).length > 0) {
+    if (mode !== "preview" && Object.keys(pipelinePatch).length > 0) {
       steps.push(actionArgsListStep("pipelineSettings", "issue_inventory", "savePipelineSettings", [
         id,
         pipelinePatch,
       ]));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Path to Merge orchestrator reads conflictStrategy / forceFinalizeMode /
// earlyMergeOnGreen / autoMerge / maxRounds / mergeMethod from saved
// PipelineSettings, not from the launch args. Persist any user-supplied
// overrides before the resolver step so the loop picks them up.
const pipelinePatch = readPipelineSettingsPatch(args);
const steps: InvocationStep[] = [];
if (maxRounds != null || autoMerge || noAutoMerge || mergeMethod) {
if (Object.keys(pipelinePatch).length > 0) {
steps.push(actionArgsListStep("pipelineSettings", "issue_inventory", "savePipelineSettings", [
id,
{
...(maxRounds != null ? { maxRounds } : {}),
...(autoMerge || noAutoMerge ? { autoMerge: autoMerge && !noAutoMerge } : {}),
...(mergeMethod ? { mergeMethod } : {}),
},
pipelinePatch,
]));
}
steps.push(actionCallStep("result", mode === "preview" ? "pr_preview_issue_resolution_prompt" : "pr_start_issue_resolution", collectGenericObjectArgs(args, input)));
const pipelinePatch = readPipelineSettingsPatch(args);
const steps: InvocationStep[] = [];
if (mode !== "preview" && Object.keys(pipelinePatch).length > 0) {
steps.push(actionArgsListStep("pipelineSettings", "issue_inventory", "savePipelineSettings", [
id,
pipelinePatch,
]));
}
steps.push(actionCallStep("result", mode === "preview" ? "pr_preview_issue_resolution_prompt" : "pr_start_issue_resolution", collectGenericObjectArgs(args, input)));

Expand All @@ -1887,12 +1953,7 @@ function buildPrPlan(args: string[]): CliPlan {
const id = requireValue(prId ?? firstPositional(args), "prId");
if (mode === "get") return { kind: "execute", label: "PR pipeline", steps: [actionArgsListStep("result", "issue_inventory", "getPipelineSettings", [id])] };
if (mode === "delete") return { kind: "execute", label: "PR pipeline delete", steps: [actionArgsListStep("result", "issue_inventory", "deletePipelineSettings", [id])] };
const maxRounds = readIntOption(args, ["--max-rounds", "--rounds"]);
const mergeMethod = readValue(args, ["--merge-method"]);
const settings = collectGenericObjectArgs(args, {
...(maxRounds != null ? { maxRounds } : {}),
...(mergeMethod ? { mergeMethod } : {}),
});
const settings = collectGenericObjectArgs(args, readPipelineSettingsPatch(args));
return { kind: "execute", label: "PR pipeline save", steps: [actionArgsListStep("result", "issue_inventory", "savePipelineSettings", [id, settings])] };
}

Expand Down
25 changes: 25 additions & 0 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { createPrService } from "./services/prs/prService";
import { createPrPollingService } from "./services/prs/prPollingService";
import { createQueueLandingService } from "./services/prs/queueLandingService";
import { createIssueInventoryService } from "./services/prs/issueInventoryService";
import { createPathToMergeOrchestrator } from "./services/prs/pathToMergeOrchestrator";
import { createPrSummaryService } from "./services/prs/prSummaryService";
import {
detectDefaultBaseRef,
Expand Down Expand Up @@ -2380,6 +2381,27 @@ app.whenReady().then(async () => {
// Wire agentChatService into prService for integration resolution
prService.setAgentChatService(agentChatService);

const pathToMergeOrchestrator = createPathToMergeOrchestrator({
logger,
prService,
laneService,
agentChatService,
sessionService,
issueInventoryService,
conflictService,
defaultModelId: null,
defaultReasoningEffort: null,
});
setImmediate(() => {
try {
pathToMergeOrchestrator.resumeFromPersistedState();
} catch (err) {
logger.warn("path_to_merge.resume_failed", {
error: err instanceof Error ? err.message : String(err),
});
}
});
Comment on lines +2384 to +2403
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dispose the path-to-merge orchestrator during context teardown.

The orchestrator is now resumed from persisted state, but it is not disposed in disposeContextResources. That can leave timers/background convergence activity running against torn-down services after project switch/close.

Proposed fix
--- a/apps/desktop/src/main/main.ts
+++ b/apps/desktop/src/main/main.ts
@@
   try {
+    ctx.pathToMergeOrchestrator?.dispose?.();
+  } catch {
+    // ignore
+  }
+  try {
     ctx.prPollingService.dispose();
   } catch {
     // ignore
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/main.ts` around lines 2384 - 2403, The path-to-merge
orchestrator created by createPathToMergeOrchestrator (variable
pathToMergeOrchestrator) is resumed but never disposed, so add teardown logic to
dispose it in disposeContextResources: store pathToMergeOrchestrator on the same
context/resource holder used by other services, and in disposeContextResources
call pathToMergeOrchestrator.dispose() (guarded for existence and wrapped in
try/catch to log any errors) to stop timers/background work when the
project/context is closed.


const gitService = createGitOperationsService({
laneService,
operationService,
Expand Down Expand Up @@ -2793,6 +2815,7 @@ app.whenReady().then(async () => {
conflictService,
prService,
issueInventoryService,
pathToMergeOrchestrator,
queueLandingService,
sessionService,
ptyService,
Expand Down Expand Up @@ -3629,6 +3652,7 @@ app.whenReady().then(async () => {
appControlService,
queueLandingService,
issueInventoryService,
pathToMergeOrchestrator,
prSummaryService,
reviewService,
jobEngine,
Expand Down Expand Up @@ -3739,6 +3763,7 @@ app.whenReady().then(async () => {
prPollingService: null,
queueLandingService: null,
issueInventoryService: null,
pathToMergeOrchestrator: null,
prSummaryService: null,
reviewService: null,
jobEngine: null,
Expand Down
Loading
Loading