Skip to content

Commit ed40569

Browse files
arul28cursoragentclaude
authored
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 <arul28@users.noreply.github.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Arul Sharma <arul28@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent df62f66 commit ed40569

69 files changed

Lines changed: 6811 additions & 181 deletions

Some content is hidden

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

apps/ade-cli/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +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
6265
ade diff patch --lane lane-id --path src/file.ts --text
6366
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
6468
ade prs list-open --text
6569
ade prs path-to-merge --pr pr-id --model gpt-5.5 --max-rounds 3 --no-auto-merge
6670
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+
email: "arul@example.com",
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: [] })),
@@ -1465,6 +1517,7 @@ describe("adeRpcServer", () => {
14651517
"pr_rerun_failed_checks",
14661518
"pr_reply_to_review_thread",
14671519
"pr_resolve_review_thread",
1520+
"getLinearQuickView",
14681521
"listLinearWorkflows",
14691522
"getLinearRunStatus",
14701523
"getLinearSyncDashboard",
@@ -1523,6 +1576,78 @@ describe("adeRpcServer", () => {
15231576
);
15241577
});
15251578

1579+
it("returns the Linear quick view for cto callers", async () => {
1580+
const { runtime } = createRuntime();
1581+
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
1582+
1583+
await initialize(handler, { callerId: "cto-1", role: "cto" });
1584+
const result = await callTool(handler, "getLinearQuickView", {});
1585+
1586+
expect((runtime.linearIssueTracker as any).getConnectionStatus).toHaveBeenCalled();
1587+
expect((runtime.linearIssueTracker as any).getQuickView).toHaveBeenCalledWith(
1588+
expect.objectContaining({
1589+
connected: true,
1590+
tokenStored: true,
1591+
viewerId: "user-1",
1592+
}),
1593+
);
1594+
expect(result.structuredContent).toEqual(
1595+
expect.objectContaining({
1596+
organization: expect.objectContaining({ name: "ADE" }),
1597+
sdk: expect.objectContaining({ packageName: "@linear/sdk" }),
1598+
}),
1599+
);
1600+
});
1601+
1602+
it("returns the Linear issue picker data for cto callers", async () => {
1603+
const { runtime } = createRuntime();
1604+
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
1605+
1606+
await initialize(handler, { callerId: "cto-1", role: "cto" });
1607+
const result = await callTool(handler, "getLinearIssuePickerData", {});
1608+
1609+
expect((runtime.linearIssueTracker as any).listProjects).toHaveBeenCalled();
1610+
expect((runtime.linearIssueTracker as any).listUsers).toHaveBeenCalled();
1611+
expect((runtime.linearIssueTracker as any).listWorkflowStates).toHaveBeenCalled();
1612+
expect(result.structuredContent).toEqual(
1613+
expect.objectContaining({
1614+
projects: expect.any(Array),
1615+
users: expect.any(Array),
1616+
states: expect.any(Array),
1617+
}),
1618+
);
1619+
});
1620+
1621+
it("forwards search filters when calling searchLinearIssues", async () => {
1622+
const { runtime } = createRuntime();
1623+
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
1624+
1625+
await initialize(handler, { callerId: "cto-1", role: "cto" });
1626+
const result = await callTool(handler, "searchLinearIssues", {
1627+
projectId: "proj-1",
1628+
stateTypes: ["started", "unstarted"],
1629+
query: "auth",
1630+
first: 25,
1631+
includeArchived: true,
1632+
});
1633+
1634+
expect((runtime.linearIssueTracker as any).searchIssues).toHaveBeenCalledWith(
1635+
expect.objectContaining({
1636+
projectId: "proj-1",
1637+
stateTypes: ["started", "unstarted"],
1638+
query: "auth",
1639+
first: 25,
1640+
includeArchived: true,
1641+
}),
1642+
);
1643+
expect(result.structuredContent).toEqual(
1644+
expect.objectContaining({
1645+
issues: expect.any(Array),
1646+
pageInfo: expect.objectContaining({ hasNextPage: false }),
1647+
}),
1648+
);
1649+
});
1650+
15261651
it("forwards employeeOverride and laneId when resuming a Linear sync queue item", async () => {
15271652
const { runtime } = createRuntime();
15281653
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
@@ -3865,6 +3990,7 @@ describe("adeRpcServer", () => {
38653990
title: "My PR",
38663991
body: "Body text",
38673992
draft: true,
3993+
closeLinearIssueOnMerge: true,
38683994
});
38693995
expect(created?.isError).toBeUndefined();
38703996
expect(fixture.runtime.prService.createFromLane).toHaveBeenCalledWith({
@@ -3873,6 +3999,7 @@ describe("adeRpcServer", () => {
38733999
title: "My PR",
38744000
body: "Body text",
38754001
draft: true,
4002+
closeLinearIssueOnMerge: true,
38764003
});
38774004

38784005
const drafted = await callTool(handler, "create_pr_from_lane", {
@@ -4259,6 +4386,65 @@ describe("adeRpcServer", () => {
42594386
);
42604387
});
42614388

4389+
it("passes branch and Linear issue data through create_lane", async () => {
4390+
const fixture = createRuntime();
4391+
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
4392+
const linearIssue = {
4393+
id: "issue-1",
4394+
identifier: "ADE-123",
4395+
title: "Create linked lane",
4396+
description: null,
4397+
url: "https://linear.app/ade/issue/ADE-123/create-linked-lane",
4398+
projectId: "project-1",
4399+
projectSlug: "ade",
4400+
projectName: "ADE",
4401+
teamId: "team-1",
4402+
teamKey: "ADE",
4403+
teamName: "ADE",
4404+
stateId: "state-1",
4405+
stateName: "Todo",
4406+
stateType: "unstarted",
4407+
priority: 2,
4408+
priorityLabel: "high",
4409+
labels: ["desktop"],
4410+
assigneeId: null,
4411+
assigneeName: null,
4412+
creatorId: null,
4413+
creatorName: null,
4414+
dueDate: null,
4415+
estimate: null,
4416+
branchName: "ade-123-create-linked-lane",
4417+
createdAt: "2026-05-08T00:00:00.000Z",
4418+
updatedAt: "2026-05-08T00:00:00.000Z",
4419+
secretToken: "do-not-forward",
4420+
};
4421+
4422+
await initialize(handler, { callerId: "orchestrator", role: "orchestrator" });
4423+
const response = await callTool(handler, "create_lane", {
4424+
name: "new-feature",
4425+
baseBranch: "main",
4426+
branchName: "ade-123-create-linked-lane",
4427+
linearIssue,
4428+
});
4429+
4430+
expect(response?.isError).toBeUndefined();
4431+
expect(fixture.runtime.laneService.create).toHaveBeenCalledWith(
4432+
expect.objectContaining({
4433+
name: "new-feature",
4434+
baseBranch: "main",
4435+
branchName: "ade-123-create-linked-lane",
4436+
linearIssue: expect.objectContaining({
4437+
id: "issue-1",
4438+
identifier: "ADE-123",
4439+
title: "Create linked lane",
4440+
projectId: "project-1",
4441+
priorityLabel: "high",
4442+
}),
4443+
})
4444+
);
4445+
expect((fixture.runtime.laneService.create as any).mock.calls[0][0].linearIssue).not.toHaveProperty("secretToken");
4446+
});
4447+
42624448
it("routes simulate_integration as a read-only dry-merge", async () => {
42634449
const fixture = createRuntime();
42644450
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

0 commit comments

Comments
 (0)