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
39 changes: 34 additions & 5 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,14 +700,18 @@ const TOOL_SPECS: ToolSpec[] = [
},
{
name: "git_checkout_branch",
description: "Checkout an existing branch in a lane checkout.",
description: "Switch a lane checkout to an existing branch or create a new branch in that lane.",
inputSchema: {
type: "object",
required: ["branchName"],
additionalProperties: false,
properties: {
laneId: { type: "string", minLength: 1 },
branchName: { type: "string", minLength: 1 }
branchName: { type: "string", minLength: 1 },
mode: { type: "string", enum: ["existing", "create"] },
startPoint: { type: "string", minLength: 1 },
baseRef: { type: "string", minLength: 1 },
acknowledgeActiveWork: { type: "boolean" }
}
Comment thread
arul28 marked this conversation as resolved.
}
},
Expand Down Expand Up @@ -2006,8 +2010,11 @@ function sanitizeToolSchema(schema: unknown): unknown {
}
if (out.type === "object" && isRecord(out.properties)) {
const propKeys = Object.keys(out.properties);
if (propKeys.length && (!Array.isArray(out.required) || !propKeys.every((k) => (out.required as string[]).includes(k)))) {
out.required = propKeys;
if (propKeys.length && !Array.isArray(out.required)) {
// Default to no required fields when none declared; preserve any
// explicit `required` array exactly as written so optional properties
// stay optional.
out.required = [];
}
const sanitizedProps: Record<string, unknown> = {};
for (const [key, val] of Object.entries(out.properties)) {
Expand Down Expand Up @@ -5258,7 +5265,29 @@ async function runTool(args: {
if (name === "git_checkout_branch") {
const laneId = requireLaneIdForTool(runtime, session, toolArgs, "git_checkout_branch");
const branchName = assertNonEmptyString(toolArgs.branchName, "branchName");
const action = await runtime.gitService.checkoutBranch({ laneId, branchName });
const rawMode = toolArgs.mode;
let mode: "existing" | "create" | undefined;
if (rawMode === undefined || rawMode === null) {
mode = undefined;
} else if (rawMode === "existing" || rawMode === "create") {
mode = rawMode;
} else {
throw new JsonRpcError(
JsonRpcErrorCode.invalidParams,
`mode must be either "existing" or "create"`
);
}
const startPoint = typeof toolArgs.startPoint === "string" ? toolArgs.startPoint : undefined;
const baseRef = typeof toolArgs.baseRef === "string" ? toolArgs.baseRef : undefined;
const acknowledgeActiveWork = typeof toolArgs.acknowledgeActiveWork === "boolean" ? toolArgs.acknowledgeActiveWork : undefined;
const action = await runtime.gitService.checkoutBranch({
laneId,
branchName,
mode: mode ?? "existing",
startPoint,
baseRef,
acknowledgeActiveWork,
});
return { laneId, branchName, action };
}

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

it("maps `git checkout <branch>` to git_checkout_branch with mode=existing by default", () => {
const plan = buildCliPlan(["git", "checkout", "feature/foo", "--lane", "lane-1"]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;

expect(plan.steps[0]?.params).toEqual({
name: "git_checkout_branch",
arguments: {
laneId: "lane-1",
branchName: "feature/foo",
mode: "existing",
acknowledgeActiveWork: false,
},
});
});

it("maps `git checkout --create` to mode=create with optional --from/--base", () => {
const plan = buildCliPlan([
"git", "checkout",
"feature/new",
"--lane", "lane-1",
"--create",
"--from", "main",
"--base", "main",
"--ack-active-work",
]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;

expect(plan.steps[0]?.params).toEqual({
name: "git_checkout_branch",
arguments: {
laneId: "lane-1",
branchName: "feature/new",
mode: "create",
startPoint: "main",
baseRef: "main",
acknowledgeActiveWork: true,
},
});
});

it("accepts the `-b` short flag as an alias for --create", () => {
const plan = buildCliPlan(["git", "checkout", "topic-1", "--lane", "lane-1", "-b"]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;
const args = (plan.steps[0]?.params as { arguments: Record<string, unknown> }).arguments;
expect(args.mode).toBe("create");
expect(args.branchName).toBe("topic-1");
});

it("omits startPoint and baseRef from the call when not supplied", () => {
const plan = buildCliPlan(["git", "checkout", "feature/x", "--lane", "lane-1"]);
expect(plan.kind).toBe("execute");
if (plan.kind !== "execute") return;
const args = (plan.steps[0]?.params as { arguments: Record<string, unknown> }).arguments;
expect(args).not.toHaveProperty("startPoint");
expect(args).not.toHaveProperty("baseRef");
});

it("rejects `git checkout` without a branch name", () => {
expect(() => buildCliPlan(["git", "checkout", "--lane", "lane-1"]))
.toThrow(/branchName/);
});

it("shows command help from subcommand help flags", () => {
const prsHelp = buildCliPlan(["prs", "create", "--help"]);
expect(prsHelp.kind).toBe("help");
Expand Down
22 changes: 18 additions & 4 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,21 @@ function buildGitPlan(args: string[]): CliPlan {
if (sub === "branches" || sub === "branch") return { kind: "execute", label: "git branches", steps: [actionCallStep("result", "git_list_branches", withLane())] };
if (sub === "checkout") {
const branchName = requireValue(readValue(args, ["--branch", "--branch-name"]) ?? firstPositional(args), "branchName");
return { kind: "execute", label: "git checkout", steps: [actionCallStep("result", "git_checkout_branch", withLane({ branchName }))] };
const create = readFlag(args, ["--create", "-b"]);
const startPoint = readValue(args, ["--start-point", "--from"]);
const baseRef = readValue(args, ["--base", "--base-ref"]);
const acknowledgeActiveWork = readFlag(args, ["--ack-active-work"]);
Comment thread
arul28 marked this conversation as resolved.
return {
kind: "execute",
label: "git checkout",
steps: [actionCallStep("result", "git_checkout_branch", withLane({
branchName,
mode: create ? "create" : "existing",
...(startPoint ? { startPoint } : {}),
...(baseRef ? { baseRef } : {}),
acknowledgeActiveWork,
}))]
};
}
if (sub === "conflicts") return { kind: "execute", label: "git conflicts", steps: [actionCallStep("result", "get_lane_conflict_state", withLane())] };
if (sub === "rebase") {
Expand Down Expand Up @@ -2031,13 +2045,13 @@ const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
"-b", "-m", "-q", "-t",
"--additional-instructions", "--app", "--arg", "--arg-json", "--arg-value",
"--arg-value-json", "--args-list-json", "--attempt", "--attempt-id",
"--automation", "--base", "--base-branch", "--body", "--branch",
"--automation", "--base", "--base-branch", "--base-ref", "--body", "--branch",
"--branch-name", "--branch-ref", "--category", "--color", "--cols",
"--command", "--comment", "--comment-id", "--commit", "--compare-ref",
"--caption", "--compare-to", "--content", "--context-file", "--cwd", "--data",
"--depth", "--desc",
"--description", "--domain", "--duration-sec", "--enabled", "--event",
"--from-file", "--group", "--group-id", "--head", "--icon", "--id",
"--from", "--from-file", "--group", "--group-id", "--head", "--icon", "--id",
"--input", "--input-json", "--instructions",
"--json-input", "--lane", "--lane-id", "--limit", "--max-bytes",
"--max-log-bytes", "--max-prompt-chars", "--max-rounds", "--memory",
Expand All @@ -2052,7 +2066,7 @@ const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
"--root-lane", "--round", "--rounds", "--rows", "--rule", "--run", "--run-id", "--scalar",
"--scalar-json", "--scope", "--seconds", "--session", "--session-id", "--set",
"--set-json", "--sha", "--source", "--source-lane", "--stack", "--stack-id",
"--stash-ref", "--step", "--step-id", "--suite", "--suite-id", "--surface",
"--start-point", "--stash-ref", "--step", "--step-id", "--suite", "--suite-id", "--surface",
"--thread", "--thread-id", "--timeout-ms", "--title", "--tool-type",
"--url", "--workspace", "--workspace-id", "--workspace-root",
]);
Expand Down
18 changes: 16 additions & 2 deletions apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2649,8 +2649,22 @@ export function createCtoOperatorTools(deps: CtoOperatorToolDeps): Record<string

tools.gitCheckoutBranch = tool({
description: "Switch to or create a git branch in a lane.",
inputSchema: z.object({ laneId: z.string().optional(), branch: z.string().min(1), create: z.boolean().optional().default(false) }),
execute: ({ laneId, branch, create }) => gitGuard(() => deps.gitService!.checkoutBranch({ laneId: resolveLaneId(laneId), branch, create })),
inputSchema: z.object({
laneId: z.string().optional(),
branch: z.string().min(1),
create: z.boolean().optional().default(false),
startPoint: z.string().optional(),
baseRef: z.string().optional(),
acknowledgeActiveWork: z.boolean().optional().default(false),
}),
execute: ({ laneId, branch, create, startPoint, baseRef, acknowledgeActiveWork }) => gitGuard(() => deps.gitService!.checkoutBranch({
laneId: resolveLaneId(laneId),
branchName: branch,
mode: create ? "create" : "existing",
startPoint,
baseRef,
acknowledgeActiveWork,
})),
});

tools.gitStashPush = tool({
Expand Down
Loading
Loading