feat(KB-093): add PR-first merge mode with configurable merge strategies
- Add mergeStrategy setting (fast-forward, squash, merge-commit) to config - Implement PR-first auto-completion flow that monitors PR merge status - Wire PR monitoring service to detect merge completion and trigger auto-close - Add PR status UI to dashboard with merge progress indicator - Update settings modal with merge strategy selector - Add changeset for PR-first merge mode feature
This commit is contained in:
@@ -10,4 +10,5 @@ export { WorktreePool, scanIdleWorktrees, cleanupOrphanedWorktrees } from "./wor
|
||||
export { createLogger, type Logger } from "./logger.js";
|
||||
export { isUsageLimitError, UsageLimitPauser } from "./usage-limit-detector.js";
|
||||
export { PrMonitor, type PrComment, type TrackedPr, type OnNewCommentsCallback } from "./pr-monitor.js";
|
||||
export { PrCommentHandler } from "./pr-comment-handler.js";
|
||||
export { NtfyNotifier, type NtfyNotifierOptions } from "./notifier.js";
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { TaskStore } from "@kb/core";
|
||||
|
||||
const mockStore = {
|
||||
addSteeringComment: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
createTask: vi.fn().mockResolvedValue({ id: "KB-123" }),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
describe("PrCommentHandler", () => {
|
||||
@@ -176,6 +176,23 @@ describe("PrCommentHandler", () => {
|
||||
"agent"
|
||||
);
|
||||
});
|
||||
|
||||
it("calls out follow-up context when feedback arrives after PR is merged", async () => {
|
||||
await handler.handleNewComments("KB-001", { ...mockPrInfo, status: "merged" }, [
|
||||
{
|
||||
id: 1,
|
||||
body: "Please add one more regression test",
|
||||
user: { login: "reviewer" },
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
html_url: "https://github.com/owner/repo/pull/42#issuecomment-1",
|
||||
},
|
||||
]);
|
||||
|
||||
const text = mockStore.addSteeringComment.mock.calls[0][1] as string;
|
||||
expect(text).toContain("This PR is already merged");
|
||||
expect(text).toContain("follow-up work");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFollowUpTask", () => {
|
||||
|
||||
@@ -128,6 +128,9 @@ export class PrCommentHandler {
|
||||
|
||||
lines.push(`**PR Review Feedback** from @${comment.user.login}`);
|
||||
lines.push(`**PR:** #${prInfo.number} (${prInfo.status})`);
|
||||
if (prInfo.status !== "open") {
|
||||
lines.push(`**Note:** This PR is already ${prInfo.status}. Treat the feedback as follow-up work.`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
// Truncate comment body if too long
|
||||
|
||||
@@ -44,6 +44,19 @@ describe("PrMonitor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("updatePrInfo", () => {
|
||||
it("updates tracked PR metadata without restarting monitoring", () => {
|
||||
monitor.startMonitoring("KB-001", "owner", "repo", mockPrInfo);
|
||||
const updatedPrInfo = { ...mockPrInfo, status: "merged" as const };
|
||||
|
||||
monitor.updatePrInfo("KB-001", updatedPrInfo);
|
||||
|
||||
const tracked = monitor.getTrackedPrs();
|
||||
expect(tracked.get("KB-001")?.prInfo.status).toBe("merged");
|
||||
expect(tracked.get("KB-001")?.owner).toBe("owner");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopMonitoring", () => {
|
||||
it("stops monitoring a task", () => {
|
||||
monitor.startMonitoring("KB-001", "owner", "repo", mockPrInfo);
|
||||
|
||||
@@ -154,6 +154,16 @@ export class PrMonitor {
|
||||
prMonitorLog.log(`Started monitoring PR #${prInfo.number} for task ${taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cached PR metadata for an already tracked task.
|
||||
* Keeps comment polling state intact while refreshing fields like PR status.
|
||||
*/
|
||||
updatePrInfo(taskId: string, prInfo: PrInfo): void {
|
||||
const tracked = this.trackedPrs.get(taskId);
|
||||
if (!tracked) return;
|
||||
tracked.prInfo = prInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring a PR.
|
||||
*/
|
||||
|
||||
@@ -150,11 +150,14 @@ export class Scheduler {
|
||||
|
||||
// Check if we're already monitoring this task
|
||||
const tracked = this.options.prMonitor.getTrackedPrs();
|
||||
if (!tracked.has(task.id)) {
|
||||
const repo = getCurrentGitHubRepo(this.store.getRootDir());
|
||||
if (repo) {
|
||||
this.options.prMonitor.startMonitoring(task.id, repo.owner, repo.repo, task.prInfo);
|
||||
}
|
||||
if (tracked.has(task.id)) {
|
||||
this.options.prMonitor.updatePrInfo(task.id, task.prInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = getCurrentGitHubRepo(this.store.getRootDir());
|
||||
if (repo) {
|
||||
this.options.prMonitor.startMonitoring(task.id, repo.owner, repo.repo, task.prInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user