diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 008ed82649..574eb622ab 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -313,8 +313,8 @@ Use Import Tasks on desktop/tablet: Expected outcome: the list pane shows matching open issues or pull requests and marks entries that already exist on the board. Use **Hide imported** beside the imported count to remove those unavailable rows from the current Issues, Pull Requests, or GitLab list; turning it off restores the greyed **Imported** rows. After a successful GitHub or GitLab import, the source row is marked **Imported** and made unavailable immediately, without waiting for the board list to refresh. 4. Select an issue or pull request row. Expected outcome: the full-width candidate list stays visible while its title, source link, body, labels or PR metadata, and import controls open in a draggable and resizable detail window. On mobile, that detail is a full-screen sheet. When selected title/body content is in another language, the detail offers **Translate**, **Show original** / **Show translation**, and **Dismiss**; translation is display-only. A pull request preview also shows its checks; each failed check has a **Create fix task** action that creates a new task prefilled with the repository, PR, branches, check status, and check-details link. -5. Select the import action in the detail window. - Expected outcome: Fusion creates a task (or review task for a pull request), preserves GitHub provenance/tracking metadata, closes the detail window, and returns to the list. +5. Select the import action in the detail window. Pull requests use **Resolve feedback**, which creates a task to resolve reviewer feedback and address failed CI checks; issues keep **Import**. Each GitHub issue and pull-request comment also has **Import as task**, which creates a separate resolve-feedback task quoting that comment and linking its source without closing the detail window. + Expected outcome: Fusion creates the requested task, preserves GitHub provenance/tracking metadata, and returns the completed PR/issue import to the list while leaving comment imports available for further feedback. Leaving and returning to **Import Tasks** (for example switching to Board and back) restores the prior context for the current project — provider (GitHub/GitLab), active Issues/PRs tab, label filter, selected repository/remote, GitLab project/group inputs, the **Hide imported** preference, and the previously selected issue/PR — instead of resetting to defaults. When GitLab integration is disabled in Settings, the GitLab provider tab is hidden and any restored GitLab provider preference opens on GitHub instead; saved GitLab URLs and tokens remain configured. The restored selection re-validates against the freshly reloaded list; a selection that no longer exists (e.g. the issue was closed upstream) clears gracefully rather than showing a stuck or empty preview. First-time opens with no prior state keep the existing default-remote auto-detect behavior. State is scoped per project and does not leak across projects. diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index f988c29176..98686ac745 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -2437,6 +2437,26 @@ export function apiImportGitHubPull(owner: string, repo: string, prNumber: numbe }); } +/** + * FNXC:GitHubImport 2026-07-16-18:05: + * Comment imports preserve the comment payload and issue/PR source context so the server can create a separately auditable resolve-feedback task without closing the detail window. + */ +export function apiImportGitHubComment( + params: { + owner: string; + repo: string; + number: number; + type: "issue" | "pull"; + comment: Pick; + }, + projectId?: string, +): Promise { + return api(withProjectId("/github/comments/import", projectId), { + method: "POST", + body: JSON.stringify(params), + }); +} + // --- GitLab Import API --- export interface GitLabImportItem { diff --git a/packages/dashboard/app/components/GitHubImportModal.css b/packages/dashboard/app/components/GitHubImportModal.css index 13a4dea36f..83c6d97e72 100644 --- a/packages/dashboard/app/components/GitHubImportModal.css +++ b/packages/dashboard/app/components/GitHubImportModal.css @@ -1263,6 +1263,32 @@ Across the thread: a top filter (All/Human/Bot) and prev/next chevrons live in t margin-left: auto; } +/* +FNXC:GitHubImport 2026-07-16-18:15: +Per-comment task import stays in the metadata flow so every human and bot comment has a reachable action without displacing author or timestamp context. Result feedback is inline because the detail window remains open for additional feedback imports. +*/ +.github-import-comment__import { + margin-left: var(--space-xs); + white-space: nowrap; +} + +.github-import-comment__import svg { + flex: 0 0 auto; +} + +.github-import-comment__result { + margin: 0 0 var(--space-xs); + font-size: var(--font-size-sm); +} + +.github-import-comment__result--success { + color: var(--color-success); +} + +.github-import-comment__result--error { + color: var(--color-error); +} + .github-import-pr-comment--active { outline: 2px solid var(--accent); outline-offset: 2px; @@ -1631,6 +1657,15 @@ Import Tasks embedded header now adopts the canonical ViewHeader chrome — edge @media (max-width: 768px) { + /* FNXC:GitHubImport 2026-07-16-18:15: Mobile detail sheets keep each comment import action on its own reachable row when metadata wraps. */ + .github-import-comment__import { + margin-left: 0; + } + + .github-import-comment__time + .github-import-comment__import { + flex-basis: 100%; + } + .github-import-provider, .github-import-gitlab .github-import-toolbar { flex-wrap: wrap; diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index ae508fd23e..a75e2f83fd 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -10,6 +10,7 @@ import { apiFetchGitHubIssueDetail, apiCloseGitHubIssue, apiImportGitHubPull, + apiImportGitHubComment, apiFetchGitLabProjectIssues, apiFetchGitLabGroupIssues, apiFetchGitLabMergeRequests, @@ -27,7 +28,7 @@ import { type GitRemote, type GitLabImportItem, } from "../api"; -import { Loader2, RefreshCw, GitPullRequest, CircleDot, ChevronUp, ChevronDown, Bot, User, Filter } from "lucide-react"; +import { Loader2, RefreshCw, GitPullRequest, CircleDot, ChevronUp, ChevronDown, Bot, User, Filter, ListPlus } from "lucide-react"; import { GithubIcon } from "./GithubIcon"; import { MailboxMessageContent } from "./MailboxMessageContent"; import { @@ -102,6 +103,12 @@ function CommentsThread({ errorTestId, emptyTestId, bodyTestId, + owner, + repo, + number, + sourceType, + projectId, + onImport, t, }: { comments: GitHubCommentDetail[]; @@ -113,6 +120,12 @@ function CommentsThread({ errorTestId: string; emptyTestId: string; bodyTestId: string; + owner: string; + repo: string; + number: number; + sourceType: "issue" | "pull"; + projectId?: string; + onImport: (task: Task) => void; t: TFunction<"app">; }) { const [filter, setFilter] = useState("all"); @@ -121,6 +134,8 @@ function CommentsThread({ const commentRefs = useRef>([]); // Avatar URLs that failed to load fall back to a generic lucide icon. const [brokenAvatars, setBrokenAvatars] = useState>(new Set()); + const [importingCommentKeys, setImportingCommentKeys] = useState>(new Set()); + const [commentImportResult, setCommentImportResult] = useState<{ key: string; kind: "success" | "error"; message: string } | null>(null); const filtered = useMemo(() => { if (filter === "human") return comments.filter((c) => !c.authorIsBot); @@ -160,6 +175,29 @@ function CommentsThread({ }); }, [scrollToIndex, filtered.length]); + /* + FNXC:GitHubImport 2026-07-16-18:10: + Each real comment row can create its own resolve-feedback task. The filtered-list index is deliberately part of the key: identical author/body/timestamp comments must retain independent loading and retry state. + */ + const handleImportComment = useCallback(async (comment: GitHubCommentDetail, key: string) => { + setImportingCommentKeys((current) => new Set(current).add(key)); + setCommentImportResult(null); + try { + const task = await apiImportGitHubComment({ owner, repo, number, type: sourceType, comment }, projectId); + onImport(task); + setCommentImportResult({ key, kind: "success", message: t("git.commentImported", "Comment imported as a task") }); + window.setTimeout(() => setCommentImportResult((current) => current?.key === key ? null : current), 4000); + } catch (err) { + setCommentImportResult({ key, kind: "error", message: getErrorMessage(err) || t("git.failedToImportComment", "Failed to import comment") }); + } finally { + setImportingCommentKeys((current) => { + const next = new Set(current); + next.delete(key); + return next; + }); + } + }, [number, onImport, owner, projectId, repo, sourceType, t]); + const renderFilter = (
{(["all", "human", "bot"] as CommentFilter[]).map((mode) => ( @@ -231,6 +269,9 @@ function CommentsThread({ const authorType = comment.authorIsBot ? "bot" : "human"; const timestamp = formatCommentTimestamp(comment.createdAt); const avatarKey = `${comment.author}-${idx}`; + const commentKey = `${sourceType}:${number}:${idx}`; + const importingComment = importingCommentKeys.has(commentKey); + const result = commentImportResult?.key === commentKey ? commentImportResult : null; const showAvatarImg = comment.authorAvatarUrl && !brokenAvatars.has(avatarKey); return (
  • )} +
  • + {result && ( +
    + {result.message} +
    + )} @@ -2009,6 +2072,12 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, errorTestId="github-import-pr-comments-error" emptyTestId="github-import-pr-comments-empty" bodyTestId="github-import-pr-comment-body" + owner={owner.trim()} + repo={repo.trim()} + number={selectedPull.number} + sourceType="pull" + projectId={projectId} + onImport={onImport} t={t} /> @@ -2022,13 +2091,8 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, ) : null} {/* - FNXC:GitHubImport 2026-07-15-23:20: - Bottom action bar for the detail preview — the ONLY place an import is committed now - (the list's footer Import was removed, so you can no longer import an issue whose body - you never opened). Import stays last/right as the primary action; Close issue sits to - its LEFT and still acts on the selected OPEN issue: hidden on the PR tab and for - already-closed issues, disabled while a close is in flight, and closing reflects - locally (the badge flips) without dismissing the preview. + FNXC:GitHubImport 2026-07-16-18:10: + The detail action remains Import for issues, while pull requests are explicitly Resolve feedback so their imported task covers reviewer feedback and failed checks. Close issue remains to its left and is unaffected. */}
    {activeTab === "issues" && selectedIssue && !selectedIssueClosed && ( @@ -2050,7 +2114,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, (activeTab === "issues" ? selectedIssueNumber === null || isUrlImported(selectedIssue?.html_url) : selectedPullNumber === null || isUrlImported(selectedPull?.html_url)) || importing } > - {importing ? : t("git.import", "Import")} + {importing ? : activeTab === "pulls" ? t("git.resolveFeedback", "Resolve feedback") : t("git.import", "Import")}
    diff --git a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx index 792b1444e3..8d26106dd7 100644 --- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx @@ -10,6 +10,7 @@ import { apiFetchGitHubIssueDetail, apiCloseGitHubIssue, apiImportGitHubPull, + apiImportGitHubComment, apiFetchGitLabProjectIssues, apiFetchGitLabGroupIssues, apiFetchGitLabMergeRequests, @@ -38,6 +39,7 @@ vi.mock("../../api", async (importOriginal) => { apiFetchGitHubIssueDetail: vi.fn(), apiCloseGitHubIssue: vi.fn(), apiImportGitHubPull: vi.fn(), + apiImportGitHubComment: vi.fn(), apiFetchGitLabProjectIssues: vi.fn(), apiFetchGitLabGroupIssues: vi.fn(), apiFetchGitLabMergeRequests: vi.fn(), @@ -171,6 +173,7 @@ describe("GitHubImportModal", () => { vi.mocked(apiFetchGitHubIssueDetail).mockReset(); vi.mocked(apiCloseGitHubIssue).mockReset(); vi.mocked(apiImportGitHubPull).mockReset(); + vi.mocked(apiImportGitHubComment).mockReset(); vi.mocked(apiFetchGitLabProjectIssues).mockReset(); vi.mocked(apiFetchGitLabGroupIssues).mockReset(); vi.mocked(apiFetchGitLabMergeRequests).mockReset(); @@ -187,6 +190,7 @@ describe("GitHubImportModal", () => { vi.mocked(apiFetchGitHubPullDetail).mockResolvedValue({ comments: [], checks: [] }); vi.mocked(apiFetchGitHubIssueDetail).mockResolvedValue({ comments: [] }); vi.mocked(apiCloseGitHubIssue).mockResolvedValue(undefined); + vi.mocked(apiImportGitHubComment).mockResolvedValue(mockTask); vi.mocked(apiFetchGitLabProjectIssues).mockResolvedValue([]); vi.mocked(apiFetchGitLabGroupIssues).mockResolvedValue([]); vi.mocked(apiFetchGitLabMergeRequests).mockResolvedValue([]); @@ -1304,6 +1308,58 @@ describe("GitHubImportModal", () => { expect(timeEl?.textContent?.length).toBeGreaterThan(0); }); + /* + FNXC:GitHubImport 2026-07-16-18:20: + PR and issue comment threads share the same import affordance; this test protects the source context passed from each parent call site and confirms bot feedback remains actionable. + */ + it("imports human and bot feedback from PR and issue comment threads", async () => { + vi.mocked(fetchGitRemotes).mockResolvedValue(singleRemote); + vi.mocked(apiFetchGitHubPulls).mockResolvedValue([{ number: 21, title: "Feedback PR", body: "body", html_url: "https://github.com/owner/repo/pull/21", headBranch: "feature", baseBranch: "main" }]); + vi.mocked(apiFetchGitHubPullDetail).mockResolvedValue({ comments: [ + { author: "reviewer", body: "Please test this", createdAt: "2026-07-16T00:00:00Z", authorIsBot: false }, + { author: "github-actions[bot]", body: "CI failed", createdAt: "2026-07-16T01:00:00Z", authorIsBot: true }, + ], checks: [] }); + + render(); + fireEvent.click(await screen.findByRole("tab", { name: /Pull Requests/i })); + fireEvent.click(await screen.findByRole("button", { name: /Select pull request #21/i })); + const prComments = await screen.findByTestId("github-import-pr-comments"); + const prButtons = await within(prComments).findAllByTestId("github-import-comment-import"); + expect(prButtons).toHaveLength(2); + fireEvent.click(prButtons[1]); + await waitFor(() => expect(vi.mocked(apiImportGitHubComment)).toHaveBeenCalledWith({ + owner: "dustinbyrne", repo: "kb", number: 21, type: "pull", + comment: { author: "github-actions[bot]", body: "CI failed", createdAt: "2026-07-16T01:00:00Z", authorIsBot: true }, + }, "project-1")); + expect(onImport).toHaveBeenCalledWith(mockTask); + expect(await within(prComments).findByText("Comment imported as a task")).toBeTruthy(); + expect(screen.getByTestId("github-import-detail-actions")).toBeTruthy(); + + vi.mocked(apiFetchGitHubIssues).mockResolvedValue([{ number: 22, title: "Feedback issue", body: "body", html_url: "https://github.com/owner/repo/issues/22", labels: [], state: "open", author: "owner" }]); + vi.mocked(apiFetchGitHubIssueDetail).mockResolvedValue({ comments: [{ author: "issue-reviewer", body: "Address this", createdAt: "2026-07-16T02:00:00Z", authorIsBot: false }] }); + fireEvent.click(screen.getByRole("tab", { name: "Issues" })); + fireEvent.click(await screen.findByRole("button", { name: /Select issue #22/i })); + const issueComments = await screen.findByTestId("github-import-issue-comments"); + fireEvent.click(await within(issueComments).findByTestId("github-import-comment-import")); + await waitFor(() => expect(vi.mocked(apiImportGitHubComment)).toHaveBeenLastCalledWith({ + owner: "dustinbyrne", repo: "kb", number: 22, type: "issue", + comment: { author: "issue-reviewer", body: "Address this", createdAt: "2026-07-16T02:00:00Z", authorIsBot: false }, + }, "project-1")); + }); + + it("shows Resolve feedback only for the pull request detail action", async () => { + vi.mocked(fetchGitRemotes).mockResolvedValue(singleRemote); + vi.mocked(apiFetchGitHubPulls).mockResolvedValue([{ number: 23, title: "Action PR", body: "body", html_url: "https://github.com/owner/repo/pull/23", headBranch: "feature", baseBranch: "main" }]); + vi.mocked(apiFetchGitHubIssues).mockResolvedValue([{ number: 24, title: "Action issue", body: "body", html_url: "https://github.com/owner/repo/issues/24", labels: [], state: "open", author: "owner" }]); + render(); + fireEvent.click(await screen.findByRole("tab", { name: /Pull Requests/i })); + fireEvent.click(await screen.findByRole("button", { name: /Select pull request #23/i })); + expect(await screen.findByRole("button", { name: "Resolve feedback" })).toBeTruthy(); + fireEvent.click(screen.getByRole("tab", { name: "Issues" })); + fireEvent.click(await screen.findByRole("button", { name: /Select issue #24/i })); + expect(await screen.findByRole("button", { name: "Import" })).toBeTruthy(); + }); + // FNXC:GitHubImport 2026-06-23-03:30: The Human filter hides bot comments; All (default) shows both. it("filters bot comments out when the comments filter is set to Human", async () => { Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1200 }); diff --git a/packages/dashboard/src/__tests__/routes-github.test.ts b/packages/dashboard/src/__tests__/routes-github.test.ts index 4481fe43fc..1b5e97d0b4 100644 --- a/packages/dashboard/src/__tests__/routes-github.test.ts +++ b/packages/dashboard/src/__tests__/routes-github.test.ts @@ -1636,6 +1636,79 @@ describe("POST /github/issues/batch-import", () => { }); }); +describe("POST /github/pulls/import and /github/comments/import", () => { + let store: TaskStore; + + beforeEach(() => { + mockIsGhAuthenticated.mockReturnValue(true); + store = createMockStore({ + listTasks: vi.fn().mockResolvedValue([]), + createTask: vi.fn().mockResolvedValue({ ...FAKE_TASK_DETAIL, id: "FN-FEEDBACK", column: "triage" }), + logEntry: vi.fn().mockResolvedValue(undefined), + }); + }); + + afterEach(() => vi.restoreAllMocks()); + + function buildApp() { + const app = express(); + app.use(express.json()); + app.use("/api", createApiRoutes(store)); + return app; + } + + it("creates resolve-feedback PR tasks while retaining provenance and deduplication", async () => { + vi.spyOn(GitHubClient.prototype, "getPullRequest").mockResolvedValue({ + number: 9, + title: "Feedback PR", + body: "PR body", + html_url: "https://github.com/owner/repo/pull/9", + headBranch: "feature/feedback", + baseBranch: "main", + state: "open", + }); + + const res = await REQUEST(buildApp(), "POST", "/api/github/pulls/import", JSON.stringify({ owner: "owner", repo: "repo", prNumber: 9 }), { + "Content-Type": "application/json", + }); + + expect(res.status).toBe(201); + expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ + title: "Resolve feedback: PR #9 — Feedback PR", + description: expect.stringContaining("Resolve the pull request review feedback and address any failed CI checks."), + })); + expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ + description: expect.stringContaining("PR: https://github.com/owner/repo/pull/9\nBranch: feature/feedback → main\n\nPR body"), + })); + }); + + it("creates comment feedback tasks without deduplicating repeated imports", async () => { + const payload = { owner: "owner", repo: "repo", number: 9, type: "pull", comment: { author: "reviewer", body: "Please add coverage", createdAt: "2026-07-16T00:00:00Z" } }; + const first = await REQUEST(buildApp(), "POST", "/api/github/comments/import", JSON.stringify(payload), { "Content-Type": "application/json" }); + const second = await REQUEST(buildApp(), "POST", "/api/github/comments/import", JSON.stringify(payload), { "Content-Type": "application/json" }); + + expect(first.status).toBe(201); + expect(second.status).toBe(201); + expect(store.createTask).toHaveBeenCalledTimes(2); + expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ + title: "Resolve feedback from @reviewer on #9", + description: "Resolve or address this feedback comment.\n\n> reviewer\n> Please add coverage\n\nSource: https://github.com/owner/repo/pull/9", + })); + expect(store.logEntry).toHaveBeenCalledWith("FN-FEEDBACK", "Imported PR/issue comment from GitHub", "https://github.com/owner/repo/pull/9"); + }); + + it("requires GitHub authentication and complete comment payloads", async () => { + const invalid = await REQUEST(buildApp(), "POST", "/api/github/comments/import", JSON.stringify({ owner: "owner" }), { "Content-Type": "application/json" }); + expect(invalid.status).toBe(400); + + mockIsGhAuthenticated.mockReturnValue(false); + const unauthenticated = await REQUEST(buildApp(), "POST", "/api/github/comments/import", JSON.stringify({ + owner: "owner", repo: "repo", number: 1, type: "issue", comment: { author: "reviewer", body: "Fix it" }, + }), { "Content-Type": "application/json" }); + expect(unauthenticated.status).toBe(401); + }); +}); + describe("projectId store scoping regressions", () => { const projectId = "proj-scoped"; let defaultStore: TaskStore; diff --git a/packages/dashboard/src/routes/register-git-github.ts b/packages/dashboard/src/routes/register-git-github.ts index f28dad6e51..78d81ea5a4 100644 --- a/packages/dashboard/src/routes/register-git-github.ts +++ b/packages/dashboard/src/routes/register-git-github.ts @@ -4787,10 +4787,13 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { } } - // Create the task with "Review PR:" prefix - const title = `Review PR #${pr.number}: ${pr.title.slice(0, 180)}`; + /* + FNXC:GitHubImport 2026-07-16-18:00: + PR imports are resolve-feedback work, so their executor prompt must explicitly cover reviewer feedback and failed CI while retaining URL, branch, and body provenance for deduplication and auditability. + */ + const title = `Resolve feedback: PR #${pr.number} — ${pr.title.slice(0, 180)}`; const body = pr.body?.trim() || "(no description)"; - const description = `Review and address any issues in this pull request.\n\nPR: ${sourceUrl}\nBranch: ${pr.headBranch} → ${pr.baseBranch}\n\n${body}`; + const description = `Resolve the pull request review feedback and address any failed CI checks.\n\nPR: ${sourceUrl}\nBranch: ${pr.headBranch} → ${pr.baseBranch}\n\n${body}`; // FNXC:Workflows 2026-07-05-00:00: FN-7611 — no workflowId here; let the store // resolve the project-default workflow's intake column (byte-identical "triage" @@ -4817,6 +4820,45 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void { } }); + /* + FNXC:GitHubImport 2026-07-16-18:00: + A reviewer or bot comment can become independent resolve-feedback work. Comments expose no stable source ID/URL, so repeated imports intentionally create separate tasks rather than applying PR-style deduplication. + */ + router.post("/github/comments/import", async (req, res) => { + try { + const { owner, repo, number, type, comment } = req.body ?? {}; + const author = comment?.author; + const body = comment?.body; + + if (!owner || typeof owner !== "string" || !owner.trim()) throw badRequest("owner is required"); + if (!repo || typeof repo !== "string" || !repo.trim()) throw badRequest("repo is required"); + if (!Number.isInteger(number) || number < 1) throw badRequest("number is required and must be a positive number"); + if (type !== "issue" && type !== "pull") throw badRequest("type must be 'issue' or 'pull'"); + if (!author || typeof author !== "string" || !author.trim()) throw badRequest("comment.author is required"); + if (!body || typeof body !== "string" || !body.trim()) throw badRequest("comment.body is required"); + if (comment.createdAt !== undefined && (typeof comment.createdAt !== "string" || !comment.createdAt.trim())) { + throw badRequest("comment.createdAt must be a non-empty string when provided"); + } + if (!isGhAuthenticated()) throw unauthorized("Not authenticated with GitHub. Run `gh auth login`."); + + const { store: scopedStore } = await getProjectContext(req); + const sourceUrl = `https://github.com/${owner.trim()}/${repo.trim()}/${type === "pull" ? "pull" : "issues"}/${number}`; + const task = await scopedStore.createTask({ + title: `Resolve feedback from @${author.trim()} on #${number}`, + description: `Resolve or address this feedback comment.\n\n> ${author.trim()}\n> ${body.trim().replace(/\n/g, "\n> ")}\n\nSource: ${sourceUrl}`, + dependencies: [], + source: { + sourceType: "github_import", + sourceMetadata: { sourceUrl, number, type, commentAuthor: author.trim(), commentCreatedAt: comment.createdAt }, + }, + }); + await scopedStore.logEntry(task.id, "Imported PR/issue comment from GitHub", sourceUrl); + res.status(201).json(task); + } catch (err: unknown) { + if (err instanceof ApiError) throw err; + rethrowAsApiError(err); + } + }); /** * POST /api/github/webhooks