diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts index b648716cf..20dfa6578 100644 --- a/src/modes/agent/index.ts +++ b/src/modes/agent/index.ts @@ -6,6 +6,8 @@ import { setupSshSigning, } from "../../github/operations/git-config"; import { checkHumanActor } from "../../github/validation/actor"; +import { createInitialComment } from "../../github/operations/comments/create-initial"; +import { isEntityContext } from "../../github/context"; import type { GitHubContext } from "../../github/context"; import type { Octokits } from "../../github/api/client"; @@ -95,6 +97,15 @@ export async function prepareAgentMode({ process.env.GITHUB_REF_NAME || defaultBranch; + // Create a sticky comment when requested and we have an entity (PR or issue) to comment on. + // Without this, the MCP comment server starts without CLAUDE_COMMENT_ID and every + // update_claude_comment call fails with "CLAUDE_COMMENT_ID environment variable is required". + let commentId: number | undefined; + if (context.inputs.useStickyComment && isEntityContext(context)) { + const commentData = await createInitialComment(octokit.rest, context); + commentId = commentData.id; + } + // Get our GitHub MCP servers config const ourMcpConfig = await prepareMcpConfig({ githubToken, @@ -102,7 +113,7 @@ export async function prepareAgentMode({ repo: context.repository.repo, branch: currentBranch, baseBranch: baseBranch, - claudeCommentId: undefined, // No tracking comment in agent mode + claudeCommentId: commentId?.toString(), allowedTools, mode: "agent", context, @@ -122,7 +133,7 @@ export async function prepareAgentMode({ claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim(); return { - commentId: undefined, + commentId, branchInfo: { baseBranch: baseBranch, currentBranch: baseBranch, // Use base branch as current when creating new branch diff --git a/test/modes/agent.test.ts b/test/modes/agent.test.ts index 1404b0d64..3e1cb2fcb 100644 --- a/test/modes/agent.test.ts +++ b/test/modes/agent.test.ts @@ -8,9 +8,13 @@ import { mock, } from "bun:test"; import { prepareAgentMode } from "../../src/modes/agent"; -import { createMockAutomationContext } from "../mockContext"; +import { + createMockAutomationContext, + createMockContext, +} from "../mockContext"; import * as core from "@actions/core"; import * as gitConfig from "../../src/github/operations/git-config"; +import * as createInitialModule from "../../src/github/operations/comments/create-initial"; describe("Agent Mode", () => { let exportVariableSpy: any; @@ -257,4 +261,125 @@ describe("Agent Mode", () => { // Should be empty or just whitespace when no MCP servers are included expect(result.claudeArgs).not.toContain("--mcp-config"); }); + + test("prepare creates sticky comment when useStickyComment is true and context is entity", async () => { + // Use entity context (pull_request) so isEntityContext returns true + const entityContext = createMockContext({ + eventName: "pull_request", + eventAction: "labeled", + isPR: true, + entityNumber: 42, + inputs: { + useStickyComment: true, + prompt: "Review this PR", + }, + }); + + const createInitialCommentSpy = spyOn( + createInitialModule, + "createInitialComment", + ).mockResolvedValue({ id: 99999 } as any); + + const mockOctokit = { + rest: { + users: { + getByUsername: mock(() => + Promise.resolve({ + data: { login: "test-user", id: 12345, type: "User" }, + }), + ), + }, + }, + } as any; + + const result = await prepareAgentMode({ + context: entityContext, + octokit: mockOctokit, + githubToken: "test-token", + }); + + expect(createInitialCommentSpy).toHaveBeenCalledTimes(1); + expect(result.commentId).toBe(99999); + + createInitialCommentSpy.mockRestore(); + }); + + test("prepare skips sticky comment when useStickyComment is false", async () => { + const entityContext = createMockContext({ + eventName: "pull_request", + eventAction: "labeled", + isPR: true, + entityNumber: 42, + inputs: { + useStickyComment: false, + prompt: "Review this PR", + }, + }); + + const createInitialCommentSpy = spyOn( + createInitialModule, + "createInitialComment", + ).mockResolvedValue({ id: 99999 } as any); + + const mockOctokit = { + rest: { + users: { + getByUsername: mock(() => + Promise.resolve({ + data: { login: "test-user", id: 12345, type: "User" }, + }), + ), + }, + }, + } as any; + + const result = await prepareAgentMode({ + context: entityContext, + octokit: mockOctokit, + githubToken: "test-token", + }); + + expect(createInitialCommentSpy).not.toHaveBeenCalled(); + expect(result.commentId).toBeUndefined(); + + createInitialCommentSpy.mockRestore(); + }); + + test("prepare skips sticky comment for non-entity events even when useStickyComment is true", async () => { + const automationContext = createMockAutomationContext({ + eventName: "workflow_dispatch", + inputs: { + useStickyComment: true, + prompt: "Run analysis", + }, + }); + + const createInitialCommentSpy = spyOn( + createInitialModule, + "createInitialComment", + ).mockResolvedValue({ id: 99999 } as any); + + const mockOctokit = { + rest: { + users: { + getByUsername: mock(() => + Promise.resolve({ + data: { login: "test-user", id: 12345, type: "User" }, + }), + ), + }, + }, + } as any; + + const result = await prepareAgentMode({ + context: automationContext, + octokit: mockOctokit, + githubToken: "test-token", + }); + + expect(createInitialCommentSpy).not.toHaveBeenCalled(); + expect(result.commentId).toBeUndefined(); + + createInitialCommentSpy.mockRestore(); + }); });