diff --git a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts
index 1eb2872b..17ecea7f 100644
--- a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts
+++ b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts
@@ -37,9 +37,97 @@ import {
} from "@open-swe/shared/open-swe/custom-node-events";
import { getDefaultHeaders } from "../../../utils/default-headers.js";
import { getCustomConfigurableFields } from "../../../utils/config.js";
+import { getGitHubTokensFromConfig } from "../../../utils/github-tokens.js";
+import {
+ createIssueComment,
+ getIssueComments,
+ updateIssueComment,
+} from "../../../utils/github/api.js";
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
+const PLAN_MESSAGE_OPEN_TAG = "";
+const PLAN_MESSAGE_CLOSE_TAG = "";
+
+function formatBodyWithPlanMessage(body: string, message: string): string {
+ if (
+ body.includes(PLAN_MESSAGE_OPEN_TAG) &&
+ body.includes(PLAN_MESSAGE_CLOSE_TAG)
+ ) {
+ const bodyBeforeTag = body.split(PLAN_MESSAGE_OPEN_TAG)[0];
+ const bodyAfterTag = body.split(PLAN_MESSAGE_CLOSE_TAG)[1];
+ const newInnerContents = `\n${PLAN_MESSAGE_OPEN_TAG}\n\n${message}\n\n${PLAN_MESSAGE_CLOSE_TAG}\n`;
+ return `${bodyBeforeTag}${newInnerContents}${bodyAfterTag}`;
+ }
+
+ return `${body}\n${PLAN_MESSAGE_OPEN_TAG}\n\n${message}\n\n${PLAN_MESSAGE_CLOSE_TAG}`;
+}
+
+function cleanTaskItems(taskItem: string): string {
+ return "```\n" + taskItem.replace("```", "\\```") + "\n```";
+}
+
+/**
+ * Posts a comment to a GitHub issue using the installation token
+ */
+async function postGitHubIssueComment(input: {
+ githubIssueId: number;
+ targetRepository: { owner: string; repo: string };
+ commentBody: string;
+ config: GraphConfig;
+}): Promise {
+ const { githubIssueId, targetRepository, commentBody, config } = input;
+ const githubAppName = process.env.GITHUB_APP_NAME;
+ if (!githubAppName) {
+ throw new Error("GITHUB_APP_NAME not set");
+ }
+
+ try {
+ const { githubInstallationToken } = getGitHubTokensFromConfig(config);
+ const existingComments = await getIssueComments({
+ owner: targetRepository.owner,
+ repo: targetRepository.repo,
+ issueNumber: githubIssueId,
+ githubInstallationToken,
+ });
+
+ const existingOpenSWEComment = existingComments?.findLast((c) =>
+ c.user?.login?.startsWith(githubAppName),
+ );
+
+ if (!existingOpenSWEComment) {
+ await createIssueComment({
+ owner: targetRepository.owner,
+ repo: targetRepository.repo,
+ issueNumber: githubIssueId,
+ body: commentBody,
+ githubToken: githubInstallationToken,
+ });
+
+ logger.info(`Posted comment to GitHub issue #${githubIssueId}`);
+ return;
+ }
+
+ // Update the comment
+ const newCommentBody = formatBodyWithPlanMessage(
+ existingOpenSWEComment.body ?? "",
+ commentBody,
+ );
+ await updateIssueComment({
+ owner: targetRepository.owner,
+ repo: targetRepository.repo,
+ commentId: existingOpenSWEComment.id,
+ body: newCommentBody,
+ githubInstallationToken,
+ });
+
+ logger.info(`Updated comment to GitHub issue #${githubIssueId}`);
+ } catch (error) {
+ logger.error("Failed to post GitHub comment:", error);
+ // Don't throw - we don't want to fail the entire process if comment posting fails
+ }
+}
+
function createAcceptedPlanMessage(input: {
planTitle: string;
planItems: PlanItem[];
@@ -161,6 +249,15 @@ export async function interruptProposedPlan(
if (state.autoAcceptPlan) {
logger.info("Auto accepting plan.");
+
+ // Post comment to GitHub issue about auto-accepting the plan
+ await postGitHubIssueComment({
+ githubIssueId: state.githubIssueId,
+ targetRepository: state.targetRepository,
+ commentBody: `### 🤖 Plan Generated\n\nI've generated a plan for this issue and will proceed to implement it since auto-accept is enabled.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${proposedPlan.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step)}`).join("\n")}\n\nProceeding to implementation...`,
+ config,
+ });
+
planItems = proposedPlan.map((p, index) => ({
index,
plan: p,
@@ -199,6 +296,14 @@ export async function interruptProposedPlan(
proposedPlan,
);
+ // Post comment to GitHub issue about plan being ready for approval
+ await postGitHubIssueComment({
+ githubIssueId: state.githubIssueId,
+ targetRepository: state.targetRepository,
+ commentBody: `### 🟠Plan Ready for Approval 🟠\n\nI've generated a plan for this issue and it's ready for your review.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${proposedPlan.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step)}`).join("\n")}\n\nPlease review the plan and let me know if you'd like me to proceed, make changes, or if you have any feedback.`,
+ config,
+ });
+
const interruptResponse = interrupt<
HumanInterrupt,
HumanResponse[] | HumanResponse
@@ -251,6 +356,14 @@ export async function interruptProposedPlan(
planItems,
{ existingTaskPlan: state.taskPlan },
);
+
+ // Update the comment to notify the user that the plan was accepted
+ await postGitHubIssueComment({
+ githubIssueId: state.githubIssueId,
+ targetRepository: state.targetRepository,
+ commentBody: `### ✅ Plan Accepted ✅\n\nThe proposed plan was accepted.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${planItems.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step.plan)}`).join("\n")}\n\nProceeding to implementation...`,
+ config,
+ });
} else if (humanResponse.type === "edit") {
const editedPlan = (humanResponse.args as ActionRequest).args.plan
.split(PLAN_INTERRUPT_DELIMITER)
@@ -268,6 +381,13 @@ export async function interruptProposedPlan(
planItems,
{ existingTaskPlan: state.taskPlan },
);
+
+ await postGitHubIssueComment({
+ githubIssueId: state.githubIssueId,
+ targetRepository: state.targetRepository,
+ commentBody: `### ✅ Plan Edited & Submitted ✅\n\nThe proposed plan was edited and submitted.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${planItems.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step.plan)}`).join("\n")}\n\nProceeding to implementation...`,
+ config,
+ });
} else {
throw new Error("Unknown interrupt type." + humanResponse.type);
}
diff --git a/apps/open-swe/src/utils/github/api.ts b/apps/open-swe/src/utils/github/api.ts
index 8a730a7c..48680002 100644
--- a/apps/open-swe/src/utils/github/api.ts
+++ b/apps/open-swe/src/utils/github/api.ts
@@ -341,3 +341,37 @@ export async function createIssueComment({
return null;
}
}
+
+export async function updateIssueComment({
+ owner,
+ repo,
+ commentId,
+ body,
+ githubInstallationToken,
+}: {
+ owner: string;
+ repo: string;
+ commentId: number;
+ body: string;
+ githubInstallationToken: string;
+}): Promise {
+ const octokit = new Octokit({
+ auth: githubInstallationToken,
+ });
+
+ try {
+ const { data: comment } = await octokit.issues.updateComment({
+ owner,
+ repo,
+ comment_id: commentId,
+ body,
+ });
+
+ return comment;
+ } catch (error) {
+ logger.error(`Failed to update issue comment`, {
+ error,
+ });
+ return null;
+ }
+}