Commit a0ee5043f9a2

Vincent Demeester <vincent@sbr.pm>
2026-02-10 16:48:27
feat: add review comment management to GitHub ext
Added 5 new actions for managing PR reviews and inline review comments: pr-reviews-list, pr-review-edit, pr-review-comments-list, pr-review-comment-edit, pr-review-comment-delete. All write operations are approval-gated. Enables editing top-level review bodies and managing individual inline comments by ID.
1 parent 034a450
Changed files (6)
dots/pi/agent/extensions/github/actions/pr.ts
@@ -7,6 +7,8 @@ import type { GhDetails, GhLineComment } from "../types";
 import {
 	parsePRList,
 	parsePRItem,
+	parseReviewComments,
+	parseReviewSummaries,
 	getErrorMessage,
 	extractPRNumber,
 	extractPRUrl,
@@ -16,8 +18,12 @@ import {
 	buildCommentConfirmation,
 	buildLineCommentConfirmation,
 	buildReviewWithCommentsConfirmation,
+	buildReviewEditConfirmation,
+	buildReviewCommentEditConfirmation,
+	buildReviewCommentDeleteConfirmation,
 	truncate,
 	getReviewDecisionText,
+	formatRelativeDate,
 } from "../utils";
 
 const PR_LIST_FIELDS = "number,title,state,author,headRefName,baseRefName,url,isDraft,labels,reviewDecision,additions,deletions,changedFiles,createdAt,updatedAt";
@@ -887,3 +893,346 @@ export async function handlePRReviewWithComments(
 		} as GhDetails,
 	};
 }
+
+// ============================================================================
+// Review listing & editing
+// ============================================================================
+
+/**
+ * List reviews on a PR (top-level review summaries with IDs)
+ */
+export async function handlePRReviewsList(
+	pi: ExtensionAPI,
+	params: any,
+	signal: AbortSignal | undefined,
+	onUpdate: any,
+	_ctx: ExtensionContext,
+): Promise<any> {
+	if (!params.number) {
+		return {
+			content: [{ type: "text", text: "Error: 'number' parameter is required" }],
+			details: { action: "pr-reviews-list", error: "missing_number" } as GhDetails,
+			isError: true,
+		};
+	}
+
+	onUpdate?.({ content: [{ type: "text", text: `Fetching reviews for PR #${params.number}...` }] });
+
+	const result = await pi.exec(
+		"gh",
+		["api", `repos/{owner}/{repo}/pulls/${params.number}/reviews`, "--paginate"],
+		{ signal, timeout: 15000 },
+	);
+
+	if (result.code !== 0) {
+		return {
+			content: [{ type: "text", text: getErrorMessage(result.stderr, "List reviews") }],
+			details: { action: "pr-reviews-list", error: result.stderr, prNumber: params.number } as GhDetails,
+			isError: true,
+		};
+	}
+
+	const reviews = parseReviewSummaries(result.stdout);
+
+	if (reviews.length === 0) {
+		return {
+			content: [{ type: "text", text: `No reviews found on PR #${params.number}` }],
+			details: { action: "pr-reviews-list", output: "empty", prNumber: params.number } as GhDetails,
+		};
+	}
+
+	let text = `Reviews on PR #${params.number}:\n\n`;
+	for (const r of reviews) {
+		const body = r.body ? `\n  ${truncate(r.body, 120)}` : "";
+		text += `• [${r.id}] ${r.state} by @${r.author} (${formatRelativeDate(r.submittedAt)})${body}\n`;
+		if (r.htmlUrl) text += `  ${r.htmlUrl}\n`;
+	}
+
+	return {
+		content: [{ type: "text", text }],
+		details: { action: "pr-reviews-list", output: `${reviews.length} reviews`, prNumber: params.number } as GhDetails,
+	};
+}
+
+/**
+ * Edit a review body (the top-level review comment, not inline comments)
+ * Uses: PUT /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}
+ */
+export async function handlePRReviewEdit(
+	pi: ExtensionAPI,
+	params: any,
+	signal: AbortSignal | undefined,
+	onUpdate: any,
+	ctx: ExtensionContext,
+): Promise<any> {
+	if (!params.number) {
+		return {
+			content: [{ type: "text", text: "Error: 'number' parameter is required" }],
+			details: { action: "pr-review-edit", error: "missing_number" } as GhDetails,
+			isError: true,
+		};
+	}
+	if (!params.reviewId) {
+		return {
+			content: [{ type: "text", text: "Error: 'reviewId' parameter is required (use pr-reviews-list to find IDs)" }],
+			details: { action: "pr-review-edit", error: "missing_review_id" } as GhDetails,
+			isError: true,
+		};
+	}
+	if (!params.body) {
+		return {
+			content: [{ type: "text", text: "Error: 'body' parameter is required (new review body text)" }],
+			details: { action: "pr-review-edit", error: "missing_body" } as GhDetails,
+			isError: true,
+		};
+	}
+
+	// APPROVAL GATE
+	if (ctx.hasUI) {
+		const confirmMessage = buildReviewEditConfirmation(params.number, params.reviewId, params.body);
+		const confirmed = await ctx.ui.confirm(`Edit review ${params.reviewId} on PR #${params.number}?`, confirmMessage);
+		if (!confirmed) {
+			ctx.ui.notify("Review edit cancelled", "info");
+			return {
+				content: [{ type: "text", text: "Review edit cancelled by user" }],
+				details: { action: "pr-review-edit", cancelled: true, prNumber: params.number } as GhDetails,
+			};
+		}
+	}
+
+	onUpdate?.({ content: [{ type: "text", text: `Editing review ${params.reviewId}...` }] });
+
+	const payload = JSON.stringify({ body: params.body });
+	const tmpFile = `/tmp/gh-review-edit-${Date.now()}.json`;
+	await pi.exec("sh", ["-c", `cat > ${tmpFile} << 'GHEOF'\n${payload}\nGHEOF`], { signal });
+
+	const result = await pi.exec(
+		"gh",
+		[
+			"api",
+			`repos/{owner}/{repo}/pulls/${params.number}/reviews/${params.reviewId}`,
+			"--method", "PUT",
+			"--input", tmpFile,
+		],
+		{ signal, timeout: 20000 },
+	);
+
+	await pi.exec("rm", ["-f", tmpFile], { signal });
+
+	if (result.code !== 0) {
+		return {
+			content: [{ type: "text", text: getErrorMessage(result.stderr, "Edit review") }],
+			details: { action: "pr-review-edit", error: result.stderr, prNumber: params.number, reviewId: params.reviewId } as GhDetails,
+			isError: true,
+		};
+	}
+
+	let url = "";
+	try {
+		const data = JSON.parse(result.stdout);
+		url = data.html_url || "";
+	} catch { /* ok */ }
+
+	return {
+		content: [{ type: "text", text: `Updated review ${params.reviewId} on PR #${params.number}${url ? "\n" + url : ""}` }],
+		details: { action: "pr-review-edit", prNumber: params.number, reviewId: params.reviewId } as GhDetails,
+	};
+}
+
+// ============================================================================
+// Inline review comment listing, editing, deleting
+// ============================================================================
+
+/**
+ * List inline review comments on a PR (with IDs for editing/deleting)
+ * Uses: GET /repos/{owner}/{repo}/pulls/{pull_number}/comments
+ */
+export async function handlePRReviewCommentsList(
+	pi: ExtensionAPI,
+	params: any,
+	signal: AbortSignal | undefined,
+	onUpdate: any,
+	_ctx: ExtensionContext,
+): Promise<any> {
+	if (!params.number) {
+		return {
+			content: [{ type: "text", text: "Error: 'number' parameter is required" }],
+			details: { action: "pr-review-comments-list", error: "missing_number" } as GhDetails,
+			isError: true,
+		};
+	}
+
+	onUpdate?.({ content: [{ type: "text", text: `Fetching review comments for PR #${params.number}...` }] });
+
+	const result = await pi.exec(
+		"gh",
+		["api", `repos/{owner}/{repo}/pulls/${params.number}/comments`, "--paginate"],
+		{ signal, timeout: 15000 },
+	);
+
+	if (result.code !== 0) {
+		return {
+			content: [{ type: "text", text: getErrorMessage(result.stderr, "List review comments") }],
+			details: { action: "pr-review-comments-list", error: result.stderr, prNumber: params.number } as GhDetails,
+			isError: true,
+		};
+	}
+
+	const comments = parseReviewComments(result.stdout);
+
+	if (comments.length === 0) {
+		return {
+			content: [{ type: "text", text: `No inline review comments on PR #${params.number}` }],
+			details: { action: "pr-review-comments-list", output: "empty", prNumber: params.number } as GhDetails,
+		};
+	}
+
+	let text = `Inline review comments on PR #${params.number}:\n\n`;
+	for (const c of comments) {
+		const reply = c.inReplyToId ? ` (reply to ${c.inReplyToId})` : "";
+		text += `• [${c.id}] ${c.path}:${c.line} by @${c.author} (${formatRelativeDate(c.createdAt)})${reply}\n`;
+		text += `  ${truncate(c.body, 120)}\n`;
+		if (c.htmlUrl) text += `  ${c.htmlUrl}\n`;
+	}
+
+	return {
+		content: [{ type: "text", text }],
+		details: { action: "pr-review-comments-list", output: `${comments.length} comments`, prNumber: params.number } as GhDetails,
+	};
+}
+
+/**
+ * Edit an inline review comment by ID
+ * Uses: PATCH /repos/{owner}/{repo}/pulls/comments/{comment_id}
+ */
+export async function handlePRReviewCommentEdit(
+	pi: ExtensionAPI,
+	params: any,
+	signal: AbortSignal | undefined,
+	onUpdate: any,
+	ctx: ExtensionContext,
+): Promise<any> {
+	if (!params.commentId) {
+		return {
+			content: [{ type: "text", text: "Error: 'commentId' parameter is required (use pr-review-comments-list to find IDs)" }],
+			details: { action: "pr-review-comment-edit", error: "missing_comment_id" } as GhDetails,
+			isError: true,
+		};
+	}
+	if (!params.body) {
+		return {
+			content: [{ type: "text", text: "Error: 'body' parameter is required (new comment text)" }],
+			details: { action: "pr-review-comment-edit", error: "missing_body" } as GhDetails,
+			isError: true,
+		};
+	}
+
+	// APPROVAL GATE
+	if (ctx.hasUI) {
+		const confirmMessage = buildReviewCommentEditConfirmation(params.commentId, params.body);
+		const confirmed = await ctx.ui.confirm(`Edit review comment ${params.commentId}?`, confirmMessage);
+		if (!confirmed) {
+			ctx.ui.notify("Edit cancelled", "info");
+			return {
+				content: [{ type: "text", text: "Review comment edit cancelled by user" }],
+				details: { action: "pr-review-comment-edit", cancelled: true, commentId: params.commentId } as GhDetails,
+			};
+		}
+	}
+
+	onUpdate?.({ content: [{ type: "text", text: `Editing comment ${params.commentId}...` }] });
+
+	const payload = JSON.stringify({ body: params.body });
+	const tmpFile = `/tmp/gh-comment-edit-${Date.now()}.json`;
+	await pi.exec("sh", ["-c", `cat > ${tmpFile} << 'GHEOF'\n${payload}\nGHEOF`], { signal });
+
+	const result = await pi.exec(
+		"gh",
+		[
+			"api",
+			`repos/{owner}/{repo}/pulls/comments/${params.commentId}`,
+			"--method", "PATCH",
+			"--input", tmpFile,
+		],
+		{ signal, timeout: 20000 },
+	);
+
+	await pi.exec("rm", ["-f", tmpFile], { signal });
+
+	if (result.code !== 0) {
+		return {
+			content: [{ type: "text", text: getErrorMessage(result.stderr, "Edit review comment") }],
+			details: { action: "pr-review-comment-edit", error: result.stderr, commentId: params.commentId } as GhDetails,
+			isError: true,
+		};
+	}
+
+	let url = "";
+	try {
+		const data = JSON.parse(result.stdout);
+		url = data.html_url || "";
+	} catch { /* ok */ }
+
+	return {
+		content: [{ type: "text", text: `Updated review comment ${params.commentId}${url ? "\n" + url : ""}` }],
+		details: { action: "pr-review-comment-edit", commentId: params.commentId } as GhDetails,
+	};
+}
+
+/**
+ * Delete an inline review comment by ID
+ * Uses: DELETE /repos/{owner}/{repo}/pulls/comments/{comment_id}
+ */
+export async function handlePRReviewCommentDelete(
+	pi: ExtensionAPI,
+	params: any,
+	signal: AbortSignal | undefined,
+	onUpdate: any,
+	ctx: ExtensionContext,
+): Promise<any> {
+	if (!params.commentId) {
+		return {
+			content: [{ type: "text", text: "Error: 'commentId' parameter is required (use pr-review-comments-list to find IDs)" }],
+			details: { action: "pr-review-comment-delete", error: "missing_comment_id" } as GhDetails,
+			isError: true,
+		};
+	}
+
+	// APPROVAL GATE
+	if (ctx.hasUI) {
+		const confirmMessage = buildReviewCommentDeleteConfirmation(params.commentId);
+		const confirmed = await ctx.ui.confirm(`Delete review comment ${params.commentId}?`, confirmMessage);
+		if (!confirmed) {
+			ctx.ui.notify("Delete cancelled", "info");
+			return {
+				content: [{ type: "text", text: "Review comment delete cancelled by user" }],
+				details: { action: "pr-review-comment-delete", cancelled: true, commentId: params.commentId } as GhDetails,
+			};
+		}
+	}
+
+	onUpdate?.({ content: [{ type: "text", text: `Deleting comment ${params.commentId}...` }] });
+
+	const result = await pi.exec(
+		"gh",
+		[
+			"api",
+			`repos/{owner}/{repo}/pulls/comments/${params.commentId}`,
+			"--method", "DELETE",
+		],
+		{ signal, timeout: 20000 },
+	);
+
+	if (result.code !== 0) {
+		return {
+			content: [{ type: "text", text: getErrorMessage(result.stderr, "Delete review comment") }],
+			details: { action: "pr-review-comment-delete", error: result.stderr, commentId: params.commentId } as GhDetails,
+			isError: true,
+		};
+	}
+
+	return {
+		content: [{ type: "text", text: `Deleted review comment ${params.commentId}` }],
+		details: { action: "pr-review-comment-delete", commentId: params.commentId } as GhDetails,
+	};
+}
dots/pi/agent/extensions/github/github.test.ts
@@ -13,6 +13,8 @@ import {
 	parseChecks,
 	parseRunList,
 	parseReviews,
+	parseReviewComments,
+	parseReviewSummaries,
 	parseReleaseList,
 	parseRepo,
 	truncate,
@@ -29,6 +31,9 @@ import {
 	buildCommentConfirmation,
 	buildLineCommentConfirmation,
 	buildReviewWithCommentsConfirmation,
+	buildReviewEditConfirmation,
+	buildReviewCommentEditConfirmation,
+	buildReviewCommentDeleteConfirmation,
 	isAuthError,
 	isNotFoundError,
 	isRepoError,
@@ -311,6 +316,91 @@ describe("Review Parsing", () => {
 	});
 });
 
+describe("Review Summary Parsing", () => {
+	test("parseReviewSummaries parses API response", () => {
+		const json = JSON.stringify([
+			{
+				id: 123456,
+				user: { login: "alice" },
+				state: "APPROVED",
+				body: "LGTM",
+				submitted_at: "2025-01-01T00:00:00Z",
+				html_url: "https://github.com/owner/repo/pull/1#pullrequestreview-123456",
+				commit_id: "abc123",
+			},
+			{
+				id: 789012,
+				user: { login: "bob" },
+				state: "CHANGES_REQUESTED",
+				body: "Fix this",
+				submitted_at: "2025-01-02T00:00:00Z",
+				html_url: "https://github.com/owner/repo/pull/1#pullrequestreview-789012",
+				commit_id: "def456",
+			},
+		]);
+
+		const summaries = parseReviewSummaries(json);
+		expect(summaries.length).toBe(2);
+		expect(summaries[0].id).toBe(123456);
+		expect(summaries[0].author).toBe("alice");
+		expect(summaries[0].state).toBe("APPROVED");
+		expect(summaries[0].htmlUrl).toContain("pullrequestreview");
+		expect(summaries[1].id).toBe(789012);
+		expect(summaries[1].state).toBe("CHANGES_REQUESTED");
+	});
+
+	test("parseReviewSummaries handles empty", () => {
+		expect(parseReviewSummaries("[]")).toEqual([]);
+		expect(parseReviewSummaries("{}")).toEqual([]);
+	});
+});
+
+describe("Review Comment Parsing (with IDs)", () => {
+	test("parseReviewComments parses API response with IDs", () => {
+		const json = JSON.stringify([
+			{
+				id: 2788725648,
+				user: { login: "vdemeester" },
+				body: "The function only checks...",
+				path: "pkg/pod/status.go",
+				line: 779,
+				created_at: "2026-02-10T15:41:54Z",
+				updated_at: "2026-02-10T15:41:54Z",
+				html_url: "https://github.com/tektoncd/pipeline/pull/9368#discussion_r2788725648",
+			},
+		]);
+
+		const comments = parseReviewComments(json);
+		expect(comments.length).toBe(1);
+		expect(comments[0].id).toBe(2788725648);
+		expect(comments[0].author).toBe("vdemeester");
+		expect(comments[0].path).toBe("pkg/pod/status.go");
+		expect(comments[0].line).toBe(779);
+		expect(comments[0].htmlUrl).toContain("discussion_r");
+	});
+
+	test("parseReviewComments handles in_reply_to_id", () => {
+		const json = JSON.stringify([
+			{
+				id: 100,
+				user: { login: "alice" },
+				body: "reply",
+				path: "main.go",
+				line: 10,
+				created_at: "2025-01-01T00:00:00Z",
+				in_reply_to_id: 99,
+			},
+		]);
+
+		const comments = parseReviewComments(json);
+		expect(comments[0].inReplyToId).toBe(99);
+	});
+
+	test("parseReviewComments handles empty", () => {
+		expect(parseReviewComments("[]")).toEqual([]);
+	});
+});
+
 describe("Release Parsing", () => {
 	test("parseReleaseList parses JSON", () => {
 		const json = JSON.stringify([
@@ -572,6 +662,27 @@ describe("Confirmation Builders", () => {
 		expect(msg).toContain("1");
 		expect(msg).not.toContain("Review body:");
 	});
+
+	test("buildReviewEditConfirmation includes all fields", () => {
+		const msg = buildReviewEditConfirmation(42, 123456, "Updated review body");
+		expect(msg).toContain("#42");
+		expect(msg).toContain("123456");
+		expect(msg).toContain("Updated review body");
+		expect(msg).toContain("update the review body");
+	});
+
+	test("buildReviewCommentEditConfirmation includes fields", () => {
+		const msg = buildReviewCommentEditConfirmation(999, "New comment text");
+		expect(msg).toContain("999");
+		expect(msg).toContain("New comment text");
+		expect(msg).toContain("update the inline review comment");
+	});
+
+	test("buildReviewCommentDeleteConfirmation includes warning", () => {
+		const msg = buildReviewCommentDeleteConfirmation(888);
+		expect(msg).toContain("888");
+		expect(msg).toContain("permanently delete");
+	});
 });
 
 // ============================================================================
dots/pi/agent/extensions/github/index.ts
@@ -33,6 +33,11 @@ import {
 	handlePRClose,
 	handlePRLineComment,
 	handlePRReviewWithComments,
+	handlePRReviewsList,
+	handlePRReviewEdit,
+	handlePRReviewCommentsList,
+	handlePRReviewCommentEdit,
+	handlePRReviewCommentDelete,
 } from "./actions/pr";
 import {
 	handleChecks,
@@ -139,13 +144,17 @@ export default function (pi: ExtensionAPI) {
 		description:
 			"Manage GitHub PRs, issues, checks, and runs via gh CLI. " +
 			"Actions: pr-list, pr-view, pr-diff, pr-create, pr-checkout, pr-merge, " +
-			"pr-review, pr-comment, pr-line-comment, pr-review-comments, pr-ready, pr-close, " +
+			"pr-review, pr-comment, pr-line-comment, pr-review-comments, " +
+			"pr-reviews-list, pr-review-edit, pr-review-comments-list, pr-review-comment-edit, pr-review-comment-delete, " +
+			"pr-ready, pr-close, " +
 			"checks, checks-log, checks-restart, run-list, run-view, " +
 			"issue-list, issue-view, issue-create, issue-close, issue-comment, issue-edit, " +
 			"repo-view, release-list. " +
 			"Use pr-line-comment to post an inline comment on a specific file/line in the diff. " +
 			"Use pr-review-comments to submit a review (approve/request-changes/comment) with multiple inline comments at once. " +
-			"Write operations (create, merge, review, comment, close, restart, edit) require user approval.",
+			"Use pr-reviews-list/pr-review-edit to list and edit top-level review bodies. " +
+			"Use pr-review-comments-list/pr-review-comment-edit/pr-review-comment-delete to manage inline review comments. " +
+			"Write operations (create, merge, review, comment, close, restart, edit, delete) require user approval.",
 
 		parameters: Type.Object({
 			action: StringEnum([
@@ -160,6 +169,11 @@ export default function (pi: ExtensionAPI) {
 				"pr-ready",
 				"pr-line-comment",
 				"pr-review-comments",
+				"pr-reviews-list",
+				"pr-review-edit",
+				"pr-review-comments-list",
+				"pr-review-comment-edit",
+				"pr-review-comment-delete",
 				"pr-close",
 				"checks",
 				"checks-log",
@@ -199,6 +213,8 @@ export default function (pi: ExtensionAPI) {
 
 			// Review
 			reviewAction: Type.Optional(Type.String({ description: "Review action: approve, request-changes, comment" })),
+			reviewId: Type.Optional(Type.Number({ description: "Review ID (from pr-reviews-list, for pr-review-edit)" })),
+			commentId: Type.Optional(Type.Number({ description: "Review comment ID (from pr-review-comments-list, for edit/delete)" })),
 
 			// Line comments (pr-line-comment, pr-review-comments)
 			path: Type.Optional(Type.String({ description: "File path in the diff for inline comment" })),
@@ -265,6 +281,16 @@ export default function (pi: ExtensionAPI) {
 						return await handlePRLineComment(pi, params, signal, onUpdate, ctx);
 					case "pr-review-comments":
 						return await handlePRReviewWithComments(pi, params, signal, onUpdate, ctx);
+					case "pr-reviews-list":
+						return await handlePRReviewsList(pi, params, signal, onUpdate, ctx);
+					case "pr-review-edit":
+						return await handlePRReviewEdit(pi, params, signal, onUpdate, ctx);
+					case "pr-review-comments-list":
+						return await handlePRReviewCommentsList(pi, params, signal, onUpdate, ctx);
+					case "pr-review-comment-edit":
+						return await handlePRReviewCommentEdit(pi, params, signal, onUpdate, ctx);
+					case "pr-review-comment-delete":
+						return await handlePRReviewCommentDelete(pi, params, signal, onUpdate, ctx);
 					case "pr-close":
 						return await handlePRClose(pi, params, signal, onUpdate, ctx);
 
@@ -399,6 +425,44 @@ export default function (pi: ExtensionAPI) {
 						0,
 						0,
 					);
+				case "pr-reviews-list":
+					return new Text(
+						theme.fg("success", "Reviews ") +
+							theme.fg("accent", `#${details.prNumber}`) +
+							theme.fg("muted", ` (${details.output})`),
+						0,
+						0,
+					);
+				case "pr-review-edit":
+					return new Text(
+						theme.fg("success", "✓ Edited review ") +
+							theme.fg("accent", `${details.reviewId}`) +
+							theme.fg("muted", ` on #${details.prNumber}`),
+						0,
+						0,
+					);
+				case "pr-review-comments-list":
+					return new Text(
+						theme.fg("success", "Review comments ") +
+							theme.fg("accent", `#${details.prNumber}`) +
+							theme.fg("muted", ` (${details.output})`),
+						0,
+						0,
+					);
+				case "pr-review-comment-edit":
+					return new Text(
+						theme.fg("success", "✓ Edited comment ") +
+							theme.fg("accent", `${details.commentId}`),
+						0,
+						0,
+					);
+				case "pr-review-comment-delete":
+					return new Text(
+						theme.fg("error", "✗ Deleted comment ") +
+							theme.fg("accent", `${details.commentId}`),
+						0,
+						0,
+					);
 				case "pr-close":
 					return new Text(theme.fg("success", "✓ Closed ") + theme.fg("accent", `#${details.prNumber}`), 0, 0);
 
dots/pi/agent/extensions/github/README.md
@@ -25,6 +25,11 @@ Manage GitHub PRs, issues, CI checks, and workflow runs directly from pi using t
 - **`pr-comment`** - Comment on PR
 - **`pr-line-comment`** - Post an inline comment on a specific file/line in the diff
 - **`pr-review-comments`** - Submit a review (approve/request-changes/comment) with multiple inline comments
+- **`pr-reviews-list`** - List reviews on a PR (with IDs for editing)
+- **`pr-review-edit`** - Edit a review body by review ID
+- **`pr-review-comments-list`** - List inline review comments (with IDs)
+- **`pr-review-comment-edit`** - Edit an inline review comment by ID
+- **`pr-review-comment-delete`** - Delete an inline review comment by ID
 - **`pr-ready`** - Mark draft as ready for review
 - **`pr-close`** - Close PR without merging
 - **`checks-restart`** - Restart failed workflow runs
@@ -118,6 +123,27 @@ https://github.com/org/repo/issues/42
 → github pr-line-comment, number=456, path=src/config.ts, line=20, startLine=15, body="..."
 ```
 
+### Managing Reviews & Review Comments
+
+```
+"List all reviews on PR 9368"
+→ github pr-reviews-list, number=9368
+  Shows review IDs, state, author, body preview
+
+"Edit my CHANGES_REQUESTED review on PR 9368 to update the body"
+→ github pr-review-edit, number=9368, reviewId=3779821940, body="Updated text..."
+
+"List all inline review comments on PR 9368"
+→ github pr-review-comments-list, number=9368
+  Shows comment IDs, file:line, author, body preview
+
+"Edit inline review comment 2788725648"
+→ github pr-review-comment-edit, commentId=2788725648, body="Updated comment..."
+
+"Delete inline review comment 2788725648"
+→ github pr-review-comment-delete, commentId=2788725648
+```
+
 ### CI/Checks
 
 ```
@@ -175,7 +201,7 @@ github/
 │   └── repo.ts           # Repo/release action handlers
 ├── types.ts              # TypeScript type definitions
 ├── utils.ts              # Parsing, formatting, error helpers
-├── github.test.ts        # Tests (67 tests)
+├── github.test.ts        # Tests (75 tests)
 ├── package.json          # Package config
 ├── Makefile              # Test runner
 └── README.md             # This file
dots/pi/agent/extensions/github/types.ts
@@ -25,8 +25,10 @@ export interface GhDetails {
 	reviewAction?: string;
 	// Merge
 	mergeMethod?: string;
-	// Line comment
+	// Line comment / review
 	commentCount?: number;
+	reviewId?: number;
+	commentId?: number;
 	// Generic
 	field?: string;
 	newValue?: string;
@@ -110,11 +112,25 @@ export interface GhReview {
 }
 
 export interface GhReviewComment {
+	id: number;
 	author: string;
 	body: string;
 	path: string;
 	line: number;
 	createdAt: string;
+	updatedAt: string;
+	inReplyToId?: number;
+	htmlUrl: string;
+}
+
+export interface GhReviewSummary {
+	id: number;
+	author: string;
+	state: string;
+	body: string;
+	submittedAt: string;
+	htmlUrl: string;
+	commitId: string;
 }
 
 export interface GhRelease {
dots/pi/agent/extensions/github/utils.ts
@@ -3,7 +3,7 @@
  */
 
 import type { Theme } from "@mariozechner/pi-coding-agent";
-import type { GhPR, GhIssue, GhCheck, GhRun, GhReview, GhReviewComment, GhRelease, GhRepo } from "./types";
+import type { GhPR, GhIssue, GhCheck, GhRun, GhReview, GhReviewComment, GhReviewSummary, GhRelease, GhRepo } from "./types";
 
 // ============================================================================
 // Parsing: gh CLI JSON output → typed objects
@@ -125,16 +125,37 @@ export function parseReviews(json: string): GhReview[] {
 export function parseReviewComments(json: string): GhReviewComment[] {
 	try {
 		const data = JSON.parse(json);
-		// gh pr view --json reviewComments isn't directly available,
-		// but gh api returns array of comments
-		const comments = data.comments ?? data;
+		const comments = Array.isArray(data) ? data : (data.comments ?? []);
 		if (!Array.isArray(comments)) return [];
 		return comments.map((item: any) => ({
+			id: item.id ?? 0,
 			author: item.author?.login ?? item.user?.login ?? "",
 			body: item.body ?? "",
 			path: item.path ?? "",
 			line: item.line ?? item.original_line ?? 0,
 			createdAt: item.createdAt ?? item.created_at ?? "",
+			updatedAt: item.updatedAt ?? item.updated_at ?? "",
+			inReplyToId: item.in_reply_to_id ?? undefined,
+			htmlUrl: item.html_url ?? "",
+		}));
+	} catch {
+		return [];
+	}
+}
+
+export function parseReviewSummaries(json: string): GhReviewSummary[] {
+	try {
+		const data = JSON.parse(json);
+		const reviews = Array.isArray(data) ? data : (data.reviews ?? []);
+		if (!Array.isArray(reviews)) return [];
+		return reviews.map((item: any) => ({
+			id: item.id ?? 0,
+			author: item.author?.login ?? item.user?.login ?? "",
+			state: item.state ?? "",
+			body: item.body ?? "",
+			submittedAt: item.submittedAt ?? item.submitted_at ?? "",
+			htmlUrl: item.html_url ?? "",
+			commitId: item.commit_id ?? "",
 		}));
 	} catch {
 		return [];
@@ -351,6 +372,29 @@ export function buildReviewWithCommentsConfirmation(number: number, reviewAction
 	return msg;
 }
 
+export function buildReviewEditConfirmation(number: number, reviewId: number, body: string): string {
+	const preview = body.length > 200 ? body.slice(0, 197) + "..." : body;
+	let msg = `PR: #${number}\n`;
+	msg += `Review ID: ${reviewId}\n\n`;
+	msg += `New body:\n"${preview}"\n\n`;
+	msg += "This will update the review body on the PR.";
+	return msg;
+}
+
+export function buildReviewCommentEditConfirmation(commentId: number, body: string): string {
+	const preview = body.length > 200 ? body.slice(0, 197) + "..." : body;
+	let msg = `Comment ID: ${commentId}\n\n`;
+	msg += `New body:\n"${preview}"\n\n`;
+	msg += "This will update the inline review comment.";
+	return msg;
+}
+
+export function buildReviewCommentDeleteConfirmation(commentId: number): string {
+	let msg = `Comment ID: ${commentId}\n\n`;
+	msg += "This will permanently delete the inline review comment.";
+	return msg;
+}
+
 export function buildCommentConfirmation(kind: string, number: number, comment: string): string {
 	const preview = comment.length > 200 ? comment.slice(0, 197) + "..." : comment;
 	let msg = `${kind}: #${number}\n\n`;