Skip to content

Commit 05505a3

Browse files
arul28claudecursoragentCopilot
authored
Remote runtime architecture spec (#275)
* Virtualized file tree, shared Ade diff viewer, and finalize (CLI + docs) (#272) * feat: virtualized file tree explorer + shared Ade diff viewer Splits FilesPage into a virtualized FilesExplorer with local path filter, inline F2 rename, and per-row git status badges, and replaces direct MonacoDiffView usage in LaneDiffPane / PrDetailPane / ChatFileChangesPanel with a shared AdeDiffViewer that supports split/unified/wrap toggles and read-only patch rendering. Diff service gains getChanges (numstat + renames) and getFilePatch with bounded output and worktree-escape checks. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * chore(finalize): allow diff.getFilePatch, CLI patch command, and docs - Extend ADE_ACTION_ALLOWLIST.diff with getFilePatch for ade actions/CLI. - Add ade diff patch wired to diff.getFilePatch; update README and tests. - Refresh internal docs for files editor, lanes diff, chat composer, architecture IPC. Co-authored-by: Arul Sharma <[email protected]> * Fix PR review feedback for diff viewer --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Arul Sharma <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> * path to merge audit (#260) * Route path-to-merge through ADE actions * Keep polling after GitHub auto-merge is armed * Remove generated dev log from PtM lane * Align PtM pipeline defaults in tests and iOS bootstrap * Respect max rounds when starting PtM from PR panel * Backfill legacy PtM pipeline defaults once * Persist PtM review bot wait state * Address PtM review automation gaps * Fix PtM desktop typecheck import * Keep admin merge behind explicit force policy * Ignore Capy spend-limit notices in PR inventory * Retire noisy PR issue comments during inventory sync * Throttle GitHub PR hot refresh polling * Limit GitHub PR hot refresh to one follow-up * Avoid GitHub snapshot refreshes for PR status ticks * Avoid duplicate review bot pings * Refresh PR detail checks on status updates * Keep action run polling results live * Fix Path to Merge readiness refresh * Fix PtM readiness test label * Make PtM readiness test less brittle * Refresh PtM external checks while polling * Reuse Vite optimizer cache in dev * Keep review gates on at-cap PtM merges * Add Linear issue dropdown to lane creation (#274) * Add Linear issue lane workflows * Fix lane git mocks for branch validation; sync iOS bootstrap SQL - Add defaultLaneBranchGitStub for check-ref-format and show-ref ade/* probes from resolveCreateBranchRef so laneService tests stub git consistently. - Drop overly broad show-ref and ls-remote stubs that broke getDeleteRisk and remote-branch checks. - Regenerate DatabaseBootstrap.sql from kvDb migrate SQL for lane_linear_issues table. Co-authored-by: Arul Sharma <[email protected]> * Add Linear issue dropdown to lane creation Surface a searchable Linear issue picker in the new-lane dialog so users can attach a Linear issue at lane creation time instead of pasting an identifier. Adds the supporting Linear browser, CLI plumbing, and doc updates for the workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ship: iteration 1 — rebase + address Greptile/CodeRabbit/Cursor review - N+1 fix: batch lane_linear_issues lookup in listLanes - GraphQL: pass IssueFilter via variables, not string interpolation - Branch sanitizer: strip @{, .., trailing .lock - Magic words: skip duplicate ID prefix on commit messages - RPC schema: nullable url/assignee* fields; validate first cap; reject non-object linearIssue payload; CLI mirrors the validation - Empty-text steer allowed when context attachments present - IPC picker/search return empty when tracker unavailable (no throw) - Lane teardown deletes lane_linear_issues; full payload validated - Adopted PR bodies now patched with Linear references too - kvDb: unique index on (project_id, lane_id) for lane_linear_issues - AgentChatPane resets context attachments on lane change - LinearIssueBadge keyboard-focusable; popover open via focus-within - LinearIssuePicker seeds pendingIssue from active selection too - CreatePrModal clears Linear close-toggle and refs when issue dropped - chatContextAttachments wraps Linear text as untrusted prompt data - CLI Linear connection status forwards organization fields * ship: iteration 2 — fix CI shards 1 & 3, align Linear RPC schema - linearAuth.test.ts: assert filter via body.variables.filter to match the variables-based GraphQL contract from iter 1 - laneService.test.ts: stub check-ref-format --branch in the runGit mock so the new branch sanitizer round-trip is allowed - kvDb.ts: replace UNIQUE index on lane_linear_issues with a bootstrap-time duplicate-coalescing sweep (CRRs disallow non-PK unique indices); app-layer enforcement remains - adeRpcServer.ts: searchLinearIssues schema first.max 200 -> 50 to match runtime clamp + error message Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ship: iteration 3 — bootstrap SQL refresh + 4 new review fixes - iOS DatabaseBootstrap.sql regenerated to track kvDb dedupe sweep - agentChatService: Codex steer uses preparedSteer.submittedText so context-only steers send the fallback prompt - agentChatService: Droid busy-steer routes through prepareSendMessage (allowActiveSession: true) like Cursor's busy path - linearClient.normalizeSdkIssue: labels now accepts resolved connection objects, not just callable thunks - prService.createFromLane: pass preserveExisting:false to ensureLinearPrReference so Refs upgrades to Fixes when closeLinearIssueOnMerge is true Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ship: iteration 4 — XML-escape untrusted text, fix adopt path Refs->Fixes, drop searchIssues min clamp - chatContextAttachments.wrapUntrustedLinearText: HTML-entity-escape &/</>/"/' before wrapping so Linear titles can't break out of the <untrusted-data> tag (Greptile P1/security) - prService adoption branch: pass preserveExisting:false to ensureLinearPrReference when closeLinearIssueOnMerge is true so Refs upgrades to Fixes on adopted PRs too (CodeRabbit Major) - linearClient.searchIssues: lower clamp 10 -> 1 to match the schema contract (Cursor Low) Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ship: iteration 5 — wrap all untrusted Linear fields, raw-GraphQL quick view, drop dead helpers - chatContextAttachments: wrap assignee/creator/team/project/state/ labels/branchName/url through wrapUntrustedLinearText so user- controlled Linear fields can't break out of the prompt sandbox (Greptile P1/security) - linearClient.getQuickView: replace SDK lazy-loaded issues calls with searchIssues raw GraphQL using ISSUE_FIELDS_FRAGMENT (was ~168 round-trips per call, now 2) (Cursor Medium) - linearClient: drop unused gqlString / gqlStringArray helpers (Cursor Low) Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Arul Sharma <[email protected]> Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> * Add remote runtime architecture spec Comprehensive engineering specification for the runtime extraction, multi-project unification, and SSH-tunneled remote runtime feature. Captures all architectural decisions, audit findings, phased implementation plan with file-level detail, migration path, and parallelization tracks for the dev team. * Address PR #275 review comments - Linear tool schema: add nullable optional properties (description, url, projectName, teamName, assigneeId, assigneeName, creatorId, creatorName, dueDate, estimate, branchName) to required so OpenAI strict mode accepts create_lane calls. The anyOf [type, null] entries already make them safely omittable at the value level. - linearClient.metadataTags: defensively read from node.metadata.tags so any populated data is preserved instead of silently dropped to []. Falls back to [] when the field is absent. - kvDb pr_pipeline_settings backfill: log via console.warn on failure instead of swallowing silently. Documents that an invisible state split (existing rows on legacy defaults vs new rows on new defaults) could otherwise occur. ALTER TABLE catches stay silent because column-already-exists is the expected re-run path. - laneService.resolveCreateBranchRef: don't blame Linear when the conflicting branch came from an explicit branchName arg or the fallback path. Differentiate the error message based on the source of the suggested branch. - bootstrap.createPathToMergeOrchestrator: replace `as never` with a typed cast through Parameters<typeof createPathToMergeOrchestrator>[0] so any future tightening of PathToMergeDeps surfaces as a type error. Also drop a gratuitous `as never` on a call whose target accepts unknown. --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Arul Sharma <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]>
1 parent 4b098fe commit 05505a3

109 files changed

Lines changed: 11658 additions & 863 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ ios-signing/
6060
.pnpm-store/
6161
/apps/desktop/.ade
6262
/.ade/shipLane/
63+
/.ade/logs/
6364
/.playwright-mcp
6465
/.codex-derived-data
6566
package-lock.json

apps/ade-cli/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,16 @@ ade auth status
5555
ade doctor
5656
ade lanes list --text
5757
ade lanes create "fix-checkout-flow" --parent main
58+
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":"..."}'
59+
ade --role cto linear quick-view --text
60+
ade --role cto linear search-issues --query "auth" --state-type started,unstarted --first 50
5861
ade git commit --lane lane-id
5962
ade git push --lane lane-id
6063
ade git branches --lane lane-id --text
6164
ade git user-identity --lane lane-id --text
65+
ade diff patch --lane lane-id --path src/file.ts --text
6266
ade prs create --lane lane-id --base main --title "Fix checkout flow"
67+
ade prs create --lane lane-id --base main --close-linear-issue-on-merge
6368
ade prs list-open --text
6469
ade prs path-to-merge --pr pr-id --model gpt-5.5 --max-rounds 3 --no-auto-merge
6570
ade prs path-to-merge --pr pr-id --model gpt-5.5 --conflict-strategy auto --force-finalize conditional

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

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,52 @@ function createRuntime() {
695695
resolveRunAction: vi.fn(async (runId: string, action: string) => ({ id: runId, status: action })),
696696
cancelRun: vi.fn(async () => {}),
697697
} as any,
698+
linearCredentialService: {
699+
getStatus: vi.fn(() => ({
700+
tokenStored: true,
701+
authMode: "manual",
702+
tokenExpiresAt: null,
703+
refreshTokenStored: false,
704+
oauthConfigured: true,
705+
})),
706+
} as any,
698707
linearIssueTracker: {
708+
getConnectionStatus: vi.fn(async () => ({
709+
connected: true,
710+
viewerId: "user-1",
711+
viewerName: "Arul",
712+
message: null,
713+
})),
714+
getQuickView: vi.fn(async (connection: unknown) => ({
715+
connection,
716+
organization: {
717+
id: "org-1",
718+
name: "ADE",
719+
urlKey: "ade",
720+
logoUrl: null,
721+
gitBranchFormat: null,
722+
createdIssueCount: 12,
723+
roadmapEnabled: true,
724+
customersEnabled: false,
725+
releasesEnabled: false,
726+
},
727+
viewer: {
728+
id: "user-1",
729+
name: "Arul",
730+
displayName: "Arul",
731+
732+
avatarUrl: null,
733+
admin: true,
734+
guest: false,
735+
url: null,
736+
},
737+
projects: [],
738+
teams: [],
739+
assignedIssues: [],
740+
recentIssues: [],
741+
fetchedAt: "2026-03-17T19:11:00.000Z",
742+
sdk: { packageName: "@linear/sdk", surfaces: ["viewer", "organization"] },
743+
})),
699744
fetchIssueById: vi.fn(async (issueId: string) => ({
700745
id: issueId,
701746
identifier: "LIN-1",
@@ -711,6 +756,13 @@ function createRuntime() {
711756
createComment: vi.fn(async () => ({ id: "comment-1" })),
712757
fetchWorkflowStates: vi.fn(async () => [{ id: "state-done", name: "Done" }]),
713758
updateIssueState: vi.fn(async () => {}),
759+
listProjects: vi.fn(async () => [{ id: "proj-1", name: "ADE", slug: "ade", teamName: "ADE", teamKey: "ADE" }]),
760+
listUsers: vi.fn(async () => [{ id: "user-1", name: "Arul", displayName: "Arul" }]),
761+
listWorkflowStates: vi.fn(async () => [{ id: "state-1", name: "Todo", type: "unstarted", teamId: "team-1", teamKey: "ADE" }]),
762+
searchIssues: vi.fn(async (query: any) => ({
763+
issues: [{ id: "issue-1", identifier: "ADE-123", title: "Test", _query: query }],
764+
pageInfo: { hasNextPage: false, endCursor: null },
765+
})),
714766
} as any,
715767
linearSyncService: {
716768
getDashboard: vi.fn(() => ({ enabled: true, running: false, ingressMode: "webhook-first", reconciliationIntervalSec: 60, lastPollAt: null, lastSuccessAt: null, lastError: null, queue: { queued: 1, blocked: 0, failed: 0 }, workflowRuns: { active: 1, waiting: 0 }, recentIssues: [] })),
@@ -1534,6 +1586,7 @@ describe("adeRpcServer", () => {
15341586
"pr_rerun_failed_checks",
15351587
"pr_reply_to_review_thread",
15361588
"pr_resolve_review_thread",
1589+
"getLinearQuickView",
15371590
"listLinearWorkflows",
15381591
"getLinearRunStatus",
15391592
"getLinearSyncDashboard",
@@ -1592,6 +1645,78 @@ describe("adeRpcServer", () => {
15921645
);
15931646
});
15941647

1648+
it("returns the Linear quick view for cto callers", async () => {
1649+
const { runtime } = createRuntime();
1650+
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
1651+
1652+
await initialize(handler, { callerId: "cto-1", role: "cto" });
1653+
const result = await callTool(handler, "getLinearQuickView", {});
1654+
1655+
expect((runtime.linearIssueTracker as any).getConnectionStatus).toHaveBeenCalled();
1656+
expect((runtime.linearIssueTracker as any).getQuickView).toHaveBeenCalledWith(
1657+
expect.objectContaining({
1658+
connected: true,
1659+
tokenStored: true,
1660+
viewerId: "user-1",
1661+
}),
1662+
);
1663+
expect(result.structuredContent).toEqual(
1664+
expect.objectContaining({
1665+
organization: expect.objectContaining({ name: "ADE" }),
1666+
sdk: expect.objectContaining({ packageName: "@linear/sdk" }),
1667+
}),
1668+
);
1669+
});
1670+
1671+
it("returns the Linear issue picker data for cto callers", async () => {
1672+
const { runtime } = createRuntime();
1673+
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
1674+
1675+
await initialize(handler, { callerId: "cto-1", role: "cto" });
1676+
const result = await callTool(handler, "getLinearIssuePickerData", {});
1677+
1678+
expect((runtime.linearIssueTracker as any).listProjects).toHaveBeenCalled();
1679+
expect((runtime.linearIssueTracker as any).listUsers).toHaveBeenCalled();
1680+
expect((runtime.linearIssueTracker as any).listWorkflowStates).toHaveBeenCalled();
1681+
expect(result.structuredContent).toEqual(
1682+
expect.objectContaining({
1683+
projects: expect.any(Array),
1684+
users: expect.any(Array),
1685+
states: expect.any(Array),
1686+
}),
1687+
);
1688+
});
1689+
1690+
it("forwards search filters when calling searchLinearIssues", async () => {
1691+
const { runtime } = createRuntime();
1692+
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
1693+
1694+
await initialize(handler, { callerId: "cto-1", role: "cto" });
1695+
const result = await callTool(handler, "searchLinearIssues", {
1696+
projectId: "proj-1",
1697+
stateTypes: ["started", "unstarted"],
1698+
query: "auth",
1699+
first: 25,
1700+
includeArchived: true,
1701+
});
1702+
1703+
expect((runtime.linearIssueTracker as any).searchIssues).toHaveBeenCalledWith(
1704+
expect.objectContaining({
1705+
projectId: "proj-1",
1706+
stateTypes: ["started", "unstarted"],
1707+
query: "auth",
1708+
first: 25,
1709+
includeArchived: true,
1710+
}),
1711+
);
1712+
expect(result.structuredContent).toEqual(
1713+
expect.objectContaining({
1714+
issues: expect.any(Array),
1715+
pageInfo: expect.objectContaining({ hasNextPage: false }),
1716+
}),
1717+
);
1718+
});
1719+
15951720
it("forwards employeeOverride and laneId when resuming a Linear sync queue item", async () => {
15961721
const { runtime } = createRuntime();
15971722
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
@@ -3934,6 +4059,7 @@ describe("adeRpcServer", () => {
39344059
title: "My PR",
39354060
body: "Body text",
39364061
draft: true,
4062+
closeLinearIssueOnMerge: true,
39374063
});
39384064
expect(created?.isError).toBeUndefined();
39394065
expect(fixture.runtime.prService.createFromLane).toHaveBeenCalledWith({
@@ -3942,6 +4068,7 @@ describe("adeRpcServer", () => {
39424068
title: "My PR",
39434069
body: "Body text",
39444070
draft: true,
4071+
closeLinearIssueOnMerge: true,
39454072
});
39464073

39474074
const drafted = await callTool(handler, "create_pr_from_lane", {
@@ -4328,6 +4455,65 @@ describe("adeRpcServer", () => {
43284455
);
43294456
});
43304457

4458+
it("passes branch and Linear issue data through create_lane", async () => {
4459+
const fixture = createRuntime();
4460+
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
4461+
const linearIssue = {
4462+
id: "issue-1",
4463+
identifier: "ADE-123",
4464+
title: "Create linked lane",
4465+
description: null,
4466+
url: "https://linear.app/ade/issue/ADE-123/create-linked-lane",
4467+
projectId: "project-1",
4468+
projectSlug: "ade",
4469+
projectName: "ADE",
4470+
teamId: "team-1",
4471+
teamKey: "ADE",
4472+
teamName: "ADE",
4473+
stateId: "state-1",
4474+
stateName: "Todo",
4475+
stateType: "unstarted",
4476+
priority: 2,
4477+
priorityLabel: "high",
4478+
labels: ["desktop"],
4479+
assigneeId: null,
4480+
assigneeName: null,
4481+
creatorId: null,
4482+
creatorName: null,
4483+
dueDate: null,
4484+
estimate: null,
4485+
branchName: "ade-123-create-linked-lane",
4486+
createdAt: "2026-05-08T00:00:00.000Z",
4487+
updatedAt: "2026-05-08T00:00:00.000Z",
4488+
secretToken: "do-not-forward",
4489+
};
4490+
4491+
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
4492+
const response = await callTool(handler, "create_lane", {
4493+
name: "new-feature",
4494+
baseBranch: "main",
4495+
branchName: "ade-123-create-linked-lane",
4496+
linearIssue,
4497+
});
4498+
4499+
expect(response?.isError).toBeUndefined();
4500+
expect(fixture.runtime.laneService.create).toHaveBeenCalledWith(
4501+
expect.objectContaining({
4502+
name: "new-feature",
4503+
baseBranch: "main",
4504+
branchName: "ade-123-create-linked-lane",
4505+
linearIssue: expect.objectContaining({
4506+
id: "issue-1",
4507+
identifier: "ADE-123",
4508+
title: "Create linked lane",
4509+
projectId: "project-1",
4510+
priorityLabel: "high",
4511+
}),
4512+
})
4513+
);
4514+
expect((fixture.runtime.laneService.create as any).mock.calls[0][0].linearIssue).not.toHaveProperty("secretToken");
4515+
});
4516+
43314517
it("routes simulate_integration as a read-only dry-merge", async () => {
43324518
const fixture = createRuntime();
43334519
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

0 commit comments

Comments
 (0)