Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ bun run format:check # Check formatting

## What This Is

A GitHub Action that lets Claude respond to `@claude` mentions on issues/PRs (tag mode) or run tasks via `prompt` input (agent mode). Mode is auto-detected: if `prompt` is provided, it's agent mode; if triggered by a comment/issue event with `@claude`, it's tag mode. See `src/modes/registry.ts`.
A GitHub Action that lets Claude respond to `@claude` mentions on issues/PRs (tag mode) or run tasks via `prompt` input (agent mode). Mode is auto-detected: if `prompt` is provided, it's agent mode; if triggered by a comment/issue event with `@claude`, it's tag mode. See `src/modes/detector.ts`.

## How It Runs

Expand All @@ -23,9 +23,9 @@ Single entrypoint: `src/entrypoints/run.ts` orchestrates everything — prepare

**Auth priority**: `github_token` input (user-provided) > GitHub App OIDC token (default). The `claude_code_oauth_token` and `anthropic_api_key` are for the Claude API, not GitHub. Token setup lives in `src/github/token.ts`.

**Mode lifecycle**: Modes implement `shouldTrigger()` → `prepare()` → `prepareContext()` → `getSystemPrompt()`. The registry in `src/modes/registry.ts` picks the mode based on event type and inputs. To add a new mode, implement the `Mode` type from `src/modes/types.ts` and register it.
**Mode lifecycle**: `detectMode()` in `src/modes/detector.ts` picks the mode name ("tag" or "agent"). Trigger checking and prepare dispatch are inlined in `run.ts`: tag mode calls `prepareTagMode()` from `src/modes/tag/`, agent mode calls `prepareAgentMode()` from `src/modes/agent/`.

**Prompt construction**: `src/prepare/` builds the prompt by fetching GitHub data (`src/github/data/fetcher.ts`), formatting it as markdown (`src/github/data/formatter.ts`), and writing it to a temp file. The prompt includes issue/PR body, comments, diff, and CI status. This is the most important part of the action — it's what Claude sees.
**Prompt construction**: Tag mode's `prepareTagMode()` builds the prompt by fetching GitHub data (`src/github/data/fetcher.ts`), formatting it as markdown (`src/github/data/formatter.ts`), and writing it to a temp file via `createPrompt()`. Agent mode writes the user's prompt directly. The prompt includes issue/PR body, comments, diff, and CI status. This is the most important part of the action — it's what Claude sees.

## Things That Will Bite You

Expand Down
60 changes: 33 additions & 27 deletions src/create-prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
import type { ParsedGitHubContext } from "../github/context";
import type { CommonFields, PreparedContext, EventData } from "./types";
import { GITHUB_SERVER_URL } from "../github/api/config";
import type { Mode, ModeContext } from "../modes/types";
import { extractUserRequest } from "../utils/extract-user-request";
export type { CommonFields, PreparedContext } from "./types";

Expand Down Expand Up @@ -458,9 +457,31 @@ export function generatePrompt(
context: PreparedContext,
githubData: FetchDataResult,
useCommitSigning: boolean,
mode: Mode,
modeName: "tag" | "agent",
): string {
return mode.generatePrompt(context, githubData, useCommitSigning);
if (modeName === "agent") {
return context.prompt || `Repository: ${context.repository}`;
}

// Tag mode
const defaultPrompt = generateDefaultPrompt(
context,
githubData,
useCommitSigning,
);

if (context.githubContext?.inputs?.prompt) {
return (
defaultPrompt +
`

<custom_instructions>
${context.githubContext.inputs.prompt}
</custom_instructions>`
);
}

return defaultPrompt;
}

/**
Expand Down Expand Up @@ -901,28 +922,20 @@ function extractUserRequestFromContext(
}

export async function createPrompt(
mode: Mode,
modeContext: ModeContext,
commentId: number,
baseBranch: string | undefined,
claudeBranch: string | undefined,
githubData: FetchDataResult,
context: ParsedGitHubContext,
) {
try {
// Prepare the context for prompt generation
let claudeCommentId: string = "";
if (mode.name === "tag") {
if (!modeContext.commentId) {
throw new Error(
`${mode.name} mode requires a comment ID for prompt generation`,
);
}
claudeCommentId = modeContext.commentId.toString();
}
const claudeCommentId = commentId.toString();

const preparedContext = prepareContext(
context,
claudeCommentId,
modeContext.baseBranch,
modeContext.claudeBranch,
baseBranch,
claudeBranch,
);

await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
Expand All @@ -934,7 +947,7 @@ export async function createPrompt(
preparedContext,
githubData,
context.inputs.useCommitSigning,
mode,
"tag",
);

// Log the final prompt to console
Expand Down Expand Up @@ -967,19 +980,12 @@ export async function createPrompt(
// Set allowed tools
const hasActionsReadPermission = false;

// Get mode-specific tools
const modeAllowedTools = mode.getAllowedTools();
const modeDisallowedTools = mode.getDisallowedTools();

const allAllowedTools = buildAllowedToolsString(
modeAllowedTools,
[],
hasActionsReadPermission,
context.inputs.useCommitSigning,
);
const allDisallowedTools = buildDisallowedToolsString(
modeDisallowedTools,
modeAllowedTools,
);
const allDisallowedTools = buildDisallowedToolsString([], []);

core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);
Expand Down
47 changes: 22 additions & 25 deletions src/entrypoints/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { setupGitHubToken } from "../github/token";
import { checkWritePermissions } from "../github/validation/permissions";
import { createOctokit } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context";
import { getMode } from "../modes/registry";
import { prepare } from "../prepare";
import { detectMode } from "../modes/detector";
import { prepareTagMode } from "../modes/tag";
import { prepareAgentMode } from "../modes/agent";
import { checkContainsTrigger } from "../github/validation/trigger";
import { collectActionInputsPresence } from "./collect-inputs";

async function run() {
Expand All @@ -22,7 +24,10 @@ async function run() {
const context = parseGitHubContext();

// Auto-detect mode based on context
const mode = getMode(context);
const modeName = detectMode(context);
console.log(
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
);

// Setup GitHub token
const githubToken = await setupGitHubToken();
Expand All @@ -46,10 +51,13 @@ async function run() {
}

// Check trigger conditions
const containsTrigger = mode.shouldTrigger(context);
const containsTrigger =
modeName === "tag"
? isEntityContext(context) && checkContainsTrigger(context)
: !!context.inputs?.prompt;

// Debug logging
console.log(`Mode: ${mode.name}`);
console.log(`Mode: ${modeName}`);
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
console.log(`Trigger result: ${containsTrigger}`);

Expand All @@ -63,31 +71,20 @@ async function run() {
return;
}

// Step 5: Use the new modular prepare function
const result = await prepare({
context,
octokit,
mode,
githubToken,
});
// Run prepare
console.log(
`Preparing with mode: ${modeName} for event: ${context.eventName}`,
);
if (modeName === "tag") {
await prepareTagMode({ context, octokit, githubToken });
} else {
await prepareAgentMode({ context, octokit, githubToken });
}

// MCP config is handled by individual modes (tag/agent) and included in their claude_args output

// Expose the GitHub token (Claude App token) as an output
core.setOutput("github_token", githubToken);

// Step 6: Get system prompt from mode if available
if (mode.getSystemPrompt) {
const modeContext = mode.prepareContext(context, {
commentId: result.commentId,
baseBranch: result.branchInfo.baseBranch,
claudeBranch: result.branchInfo.claudeBranch,
});
const systemPrompt = mode.getSystemPrompt(modeContext);
if (systemPrompt) {
core.exportVariable("APPEND_SYSTEM_PROMPT", systemPrompt);
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
Expand Down
44 changes: 20 additions & 24 deletions src/entrypoints/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import { createOctokit } from "../github/api/client";
import type { Octokits } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context";
import type { GitHubContext } from "../github/context";
import { getMode } from "../modes/registry";
import { prepare } from "../prepare";
import { detectMode } from "../modes/detector";
import { prepareTagMode } from "../modes/tag";
import { prepareAgentMode } from "../modes/agent";
import { checkContainsTrigger } from "../github/validation/trigger";
import { collectActionInputsPresence } from "./collect-inputs";
import { updateCommentLink } from "./update-comment-link";
import { formatTurnsFromData } from "./format-turns";
Expand Down Expand Up @@ -138,7 +140,10 @@ async function run() {
// Phase 1: Prepare
const actionInputsPresent = collectActionInputsPresence();
context = parseGitHubContext();
const mode = getMode(context);
const modeName = detectMode(context);
console.log(
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
);

try {
githubToken = await setupGitHubToken();
Expand Down Expand Up @@ -173,8 +178,11 @@ async function run() {
}

// Check trigger conditions
const containsTrigger = mode.shouldTrigger(context);
console.log(`Mode: ${mode.name}`);
const containsTrigger =
modeName === "tag"
? isEntityContext(context) && checkContainsTrigger(context)
: !!context.inputs?.prompt;
console.log(`Mode: ${modeName}`);
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
console.log(`Trigger result: ${containsTrigger}`);

Expand All @@ -185,31 +193,19 @@ async function run() {
}

// Run prepare
const prepareResult = await prepare({
context,
octokit,
mode,
githubToken,
});
console.log(
`Preparing with mode: ${modeName} for event: ${context.eventName}`,
);
const prepareResult =
modeName === "tag"
? await prepareTagMode({ context, octokit, githubToken })
: await prepareAgentMode({ context, octokit, githubToken });

commentId = prepareResult.commentId;
claudeBranch = prepareResult.branchInfo.claudeBranch;
baseBranch = prepareResult.branchInfo.baseBranch;
prepareCompleted = true;

// Set system prompt if available
if (mode.getSystemPrompt) {
const modeContext = mode.prepareContext(context, {
commentId: prepareResult.commentId,
baseBranch: prepareResult.branchInfo.baseBranch,
claudeBranch: prepareResult.branchInfo.claudeBranch,
});
const systemPrompt = mode.getSystemPrompt(modeContext);
if (systemPrompt) {
core.exportVariable("APPEND_SYSTEM_PROMPT", systemPrompt);
}
}

// Phase 2: Install Claude Code CLI
await installClaudeCode();

Expand Down
Loading
Loading