Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ ade init
ade lanes list --text
ade lanes create "fix-checkout-flow" --parent main
ade lanes create "lin-123" --linear-issue-json '{"id":"...","identifier":"LIN-123","title":"...","projectId":"...","projectSlug":"...","teamId":"...","teamKey":"...","stateId":"...","stateName":"Todo","stateType":"unstarted","priority":2,"priorityLabel":"high","labels":[],"assigneeId":null,"assigneeName":null,"createdAt":"...","updatedAt":"..."}'
ade lanes reparent lane-child --parent lane-parent --stack-base-branch main
ade --role cto linear quick-view --text
ade --role cto linear search-issues --query "auth" --state-type started,unstarted --first 50
ade git commit --lane lane-id
Expand All @@ -239,6 +240,7 @@ ade diff patch --lane lane-id --path src/file.ts --text
ade prs create --lane lane-id --base main --title "Fix checkout flow"
ade prs create --lane lane-id --base main --close-linear-issue-on-merge
ade prs list-open --text
ade prs github-snapshot --include-external-closed
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
Expand Down
1 change: 1 addition & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ function createRuntime() {
void data;
return true;
}),
readTranscriptTail: vi.fn(async () => ""),
enrichSessions: vi.fn((sessions: unknown[]) => sessions),
},
testService: {
Expand Down
12 changes: 8 additions & 4 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3592,9 +3592,11 @@ async function waitForSessionCompletion(args: {
while (Date.now() <= deadline) {
const session = runtime.sessionService.get(sessionId);
if (session && session.status !== "running") {
const logTail = runtime.sessionService.readTranscriptTail(session.transcriptPath, maxLogBytes, {
const logTail = await runtime.ptyService.readTranscriptTail({
sessionId,
maxBytes: maxLogBytes,
raw: true,
alignToLineBoundary: true
alignToLineBoundary: true,
});
return {
session,
Expand All @@ -3610,9 +3612,11 @@ async function waitForSessionCompletion(args: {
session,
timedOut: true,
logTail: session
? runtime.sessionService.readTranscriptTail(session.transcriptPath, maxLogBytes, {
? await runtime.ptyService.readTranscriptTail({
sessionId,
maxBytes: maxLogBytes,
raw: true,
alignToLineBoundary: true
alignToLineBoundary: true,
})
: ""
};
Expand Down
78 changes: 78 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,74 @@ describe("ADE CLI", () => {
});
});

it("forwards lane reparent stack base branch override to the runtime action", () => {
const reparent = buildCliPlan([
"lanes",
"reparent",
"lane-child",
"--parent",
"lane-parent",
"--stack-base-branch",
"develop",
]);
expect(reparent.kind).toBe("execute");
if (reparent.kind !== "execute") return;
expect(reparent.steps[0]?.params).toEqual({
name: "run_ade_action",
arguments: {
domain: "lane",
action: "reparent",
args: {
laneId: "lane-child",
newParentLaneId: "lane-parent",
stackBaseBranchRef: "develop",
},
},
});

const reparentDefault = buildCliPlan([
"lanes",
"reparent",
"lane-child",
"--parent",
"lane-parent",
]);
expect(reparentDefault.kind).toBe("execute");
if (reparentDefault.kind !== "execute") return;
expect(reparentDefault.steps[0]?.params).toEqual({
name: "run_ade_action",
arguments: {
domain: "lane",
action: "reparent",
args: {
laneId: "lane-child",
newParentLaneId: "lane-parent",
},
},
});
});

it("forwards PR GitHub snapshot full-history flag to the runtime action", () => {
const snapshot = buildCliPlan([
"prs",
"github-snapshot",
"--include-external-closed",
]);
expect(snapshot.kind).toBe("execute");
if (snapshot.kind !== "execute") return;
expect(snapshot.steps[0]?.params).toEqual({
name: "run_ade_action",
arguments: {
domain: "pr",
action: "getGithubSnapshot",
args: {
force: false,
includeExternalClosed: true,
},
},
});
});

it("maps discoverable git status, sync, and conflict helpers to existing actions", () => {
const fullStatus = buildCliPlan([
"git",
Expand Down Expand Up @@ -1854,6 +1922,16 @@ describe("ADE CLI", () => {
// Regression: --text as output flag must not swallow --help.
const lanesHelp = buildCliPlan(["lanes", "list", "--text", "--help"]);
expect(lanesHelp.kind).toBe("help");

const reparentHelp = buildCliPlan([
"lanes",
"reparent",
"lane-child",
"--stack-base-branch",
"develop",
"--help",
]);
expect(reparentHelp.kind).toBe("help");
});

it("maps PR create Linear close flag to the typed RPC tool", () => {
Expand Down
48 changes: 35 additions & 13 deletions apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,9 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade lanes archive <lane> Archive a lane in ADE
$ ade lanes unarchive <lane> Restore an archived lane
$ ade lanes attach --path <worktree> --name <n> Attach an external worktree
$ ade lanes reparent <lane> --parent <parent> Move lane onto a new parent (runs git rebase)
$ ade lanes reparent <lane> --parent <parent> --stack-base-branch <branch>
Reparent and stack onto a specific branch (e.g. origin/main)
$ ade lanes actions --text List callable lane service methods
`,
git: `${ADE_BANNER}
Expand Down Expand Up @@ -947,6 +950,8 @@ const HELP_BY_COMMAND: Record<string, string> = {
$ ade prs link --lane <lane> --url <pr-url> Map an existing GitHub PR to a lane
$ ade prs checks <pr> --text Show check status
$ ade prs comments <pr> --text Show unresolved review work
$ ade prs github-snapshot --include-external-closed
Include closed external PR history in the GitHub snapshot
$ 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
Expand Down Expand Up @@ -2478,6 +2483,23 @@ function buildLanePlan(args: string[]): CliPlan {
readLaneId(args) ?? firstPositional(args),
"laneId",
);
const reparentArgs: JsonObject = {
laneId,
newParentLaneId:
readValue(args, [
"--parent",
"--parent-lane",
"--parent-lane-id",
]) ?? firstPositional(args),
};
const stackBaseBranchRef = readValue(args, [
"--stack-base-branch",
"--stack-base",
"--base-branch-ref",
]);
if (stackBaseBranchRef != null) {
reparentArgs.stackBaseBranchRef = stackBaseBranchRef;
}
return {
kind: "execute",
label: "lane reparent",
Expand All @@ -2486,15 +2508,7 @@ function buildLanePlan(args: string[]): CliPlan {
"result",
"lane",
"reparent",
collectGenericObjectArgs(args, {
laneId,
newParentLaneId:
readValue(args, [
"--parent",
"--parent-lane",
"--parent-lane-id",
]) ?? firstPositional(args),
}),
collectGenericObjectArgs(args, reparentArgs),
),
],
};
Expand Down Expand Up @@ -3628,7 +3642,13 @@ function buildPrPlan(args: string[]): CliPlan {
label: "PR mobile snapshot",
steps: [actionArgsListStep("result", "pr", "getMobileSnapshot", [])],
};
if (sub === "github-snapshot")
if (sub === "github-snapshot") {
const snapshotArgs: JsonObject = {
force: readFlag(args, ["--force"]),
};
if (readFlag(args, ["--include-external-closed", "--include-closed-external"])) {
snapshotArgs.includeExternalClosed = true;
}
return {
kind: "execute",
label: "PR GitHub snapshot",
Expand All @@ -3637,12 +3657,11 @@ function buildPrPlan(args: string[]): CliPlan {
"result",
"pr",
"getGithubSnapshot",
collectGenericObjectArgs(args, {
force: readFlag(args, ["--force"]),
}),
collectGenericObjectArgs(args, snapshotArgs),
),
],
};
}
if (sub === "conflicts") {
const mode = firstPositional(args) ?? "list";
if (mode === "list")
Expand Down Expand Up @@ -8312,6 +8331,7 @@ const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
"--backend",
"--base",
"--base-branch",
"--base-branch-ref",
"--base-ref",
"--body",
"--branch",
Expand Down Expand Up @@ -8449,6 +8469,8 @@ const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
"--source",
"--source-lane",
"--stack",
"--stack-base",
"--stack-base-branch",
"--stack-id",
"--scheme",
"--start-point",
Expand Down
1 change: 1 addition & 0 deletions apps/ade-cli/src/services/sync/syncHostService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ function createHostArgs(projectRoot: string, projects: SyncMobileProjectSummary[
},
ptyService: {
create: vi.fn(),
readTranscriptTail: vi.fn(async () => ""),
enrichSessions: (rows: unknown[]) => rows,
},
computerUseArtifactBrokerService: {
Expand Down
11 changes: 6 additions & 5 deletions apps/ade-cli/src/services/sync/syncHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2830,11 +2830,12 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
peer.subscribedSessionIds.add(sessionId);
const session = args.sessionService.get(sessionId);
const transcript = session
? await args.sessionService.readTranscriptTail(
session.transcriptPath,
Math.max(1_024, Math.min(2_000_000, Math.floor(payload?.maxBytes ?? DEFAULT_TERMINAL_SNAPSHOT_BYTES))),
{ raw: true, alignToLineBoundary: true },
)
? await args.ptyService.readTranscriptTail({
sessionId,
maxBytes: Math.max(1_024, Math.min(2_000_000, Math.floor(payload?.maxBytes ?? DEFAULT_TERMINAL_SNAPSHOT_BYTES))),
raw: true,
alignToLineBoundary: true,
})
: "";
const snapshot: SyncTerminalSnapshotPayload = {
sessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,11 @@ function parseRenameLaneArgs(value: Record<string, unknown>): RenameLaneArgs {
}

function parseReparentLaneArgs(value: Record<string, unknown>): ReparentLaneArgs {
const stackBaseBranchRef = asTrimmedString(value.stackBaseBranchRef);
return {
laneId: requireString(value.laneId, "lanes.reparent requires laneId."),
newParentLaneId: requireString(value.newParentLaneId, "lanes.reparent requires newParentLaneId."),
...(stackBaseBranchRef ? { stackBaseBranchRef } : {}),
};
}

Expand Down Expand Up @@ -2333,7 +2335,10 @@ export function createSyncRemoteCommandService(args: SyncRemoteCommandServiceArg
register("prs.getComments", { viewerAllowed: true }, async (payload) => args.prService.getComments(requirePrId(payload, "prs.getComments")));
register("prs.getFiles", { viewerAllowed: true }, async (payload) => args.prService.getFiles(requirePrId(payload, "prs.getFiles")));
register("prs.getGitHubSnapshot", { viewerAllowed: true }, async (payload) =>
args.prService.getGithubSnapshot({ force: payload.force === true }));
args.prService.getGithubSnapshot({
force: payload.force === true,
includeExternalClosed: payload.includeExternalClosed === true,
}));
register("prs.getReviewThreads", { viewerAllowed: true }, async (payload) => args.prService.getReviewThreads(requirePrId(payload, "prs.getReviewThreads")));
register("prs.getActionRuns", { viewerAllowed: true }, async (payload) => args.prService.getActionRuns(requirePrId(payload, "prs.getActionRuns")));
register("prs.getActivity", { viewerAllowed: true }, async (payload) => args.prService.getActivity(requirePrId(payload, "prs.getActivity")));
Expand Down
79 changes: 79 additions & 0 deletions apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,85 @@ describe("subagentSnapshotsFromEvents", () => {
summary: "done",
});
});

it("adopts the resolved task id when a runtime placeholder has the same parent tool id", () => {
const snapshots = subagentSnapshotsFromEvents([
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:00.000Z",
sequence: 1,
event: {
type: "subagent_started",
taskId: "spawn-1",
parentToolUseId: "spawn-1",
description: "Parallel agent",
},
},
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:01.000Z",
sequence: 2,
event: {
type: "subagent_progress",
taskId: "thread-1",
parentToolUseId: "spawn-1",
summary: "working",
},
},
]);

expect(snapshots).toHaveLength(1);
expect(snapshots[0]).toMatchObject({
id: "thread-1",
name: "Parallel agent",
parentToolUseId: "spawn-1",
status: "running",
summary: "working",
});
});

it("stops foreground subagents when their parent turn has ended", () => {
const snapshots = subagentSnapshotsFromEvents([
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:00.000Z",
sequence: 1,
event: {
type: "subagent_started",
taskId: "agent-1",
description: "Foreground agent",
turnId: "turn-1",
},
},
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:01.000Z",
sequence: 2,
event: {
type: "subagent_started",
taskId: "agent-bg",
description: "Background agent",
background: true,
turnId: "turn-1",
},
},
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:02.000Z",
sequence: 3,
event: { type: "done", turnId: "turn-1", status: "completed" },
},
]);

expect(snapshots.find((snapshot) => snapshot.id === "agent-1")).toMatchObject({
status: "stopped",
summary: "Parent turn ended before ADE received a final subagent status",
});
expect(snapshots.find((snapshot) => snapshot.id === "agent-bg")).toMatchObject({
status: "running",
background: true,
});
});
});

describe("clampChatScrollOffsetRows", () => {
Expand Down
Loading