diff --git a/CLAUDE.md b/CLAUDE.md
index e05c2ee9c..6c636bdde 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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
@@ -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
diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts
index 66149eac3..4624c198f 100644
--- a/src/create-prompt/index.ts
+++ b/src/create-prompt/index.ts
@@ -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";
@@ -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 +
+ `
+
+
+${context.githubContext.inputs.prompt}
+`
+ );
+ }
+
+ return defaultPrompt;
}
/**
@@ -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`, {
@@ -934,7 +947,7 @@ export async function createPrompt(
preparedContext,
githubData,
context.inputs.useCommitSigning,
- mode,
+ "tag",
);
// Log the final prompt to console
@@ -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);
diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts
index af0ce9d26..a0b0aad6b 100644
--- a/src/entrypoints/prepare.ts
+++ b/src/entrypoints/prepare.ts
@@ -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() {
@@ -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();
@@ -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}`);
@@ -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}`);
diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts
index 871fa1915..54eae0139 100644
--- a/src/entrypoints/run.ts
+++ b/src/entrypoints/run.ts
@@ -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";
@@ -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();
@@ -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}`);
@@ -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();
diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts
index 0e9376c61..e6047379c 100644
--- a/src/modes/agent/index.ts
+++ b/src/modes/agent/index.ts
@@ -1,7 +1,4 @@
-import * as core from "@actions/core";
import { mkdir, writeFile } from "fs/promises";
-import type { Mode, ModeOptions, ModeResult } from "../types";
-import type { PreparedContext } from "../../create-prompt/types";
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
import { parseAllowedTools } from "./parse-tools";
import {
@@ -10,211 +7,128 @@ import {
} from "../../github/operations/git-config";
import { checkHumanActor } from "../../github/validation/actor";
import type { GitHubContext } from "../../github/context";
-import { isEntityContext } from "../../github/context";
+import type { Octokits } from "../../github/api/client";
/**
- * Extract GitHub context as environment variables for agent mode
- */
-function extractGitHubContext(context: GitHubContext): Record {
- const envVars: Record = {};
-
- // Basic repository info
- envVars.GITHUB_REPOSITORY = context.repository.full_name;
- envVars.GITHUB_TRIGGER_ACTOR = context.actor;
- envVars.GITHUB_EVENT_NAME = context.eventName;
-
- // Entity-specific context (PR/issue numbers, branches, etc.)
- if (isEntityContext(context)) {
- if (context.isPR) {
- envVars.GITHUB_PR_NUMBER = String(context.entityNumber);
-
- // Extract branch info from payload if available
- if (
- context.payload &&
- "pull_request" in context.payload &&
- context.payload.pull_request
- ) {
- envVars.GITHUB_BASE_REF = context.payload.pull_request.base?.ref || "";
- envVars.GITHUB_HEAD_REF = context.payload.pull_request.head?.ref || "";
- }
- } else {
- envVars.GITHUB_ISSUE_NUMBER = String(context.entityNumber);
- }
- }
-
- return envVars;
-}
-
-/**
- * Agent mode implementation.
+ * Prepares the agent mode execution context.
*
- * This mode runs whenever an explicit prompt is provided in the workflow configuration.
+ * Agent mode runs whenever an explicit prompt is provided in the workflow configuration.
* It bypasses the standard @claude mention checking and comment tracking used by tag mode,
* providing direct access to Claude Code for automation workflows.
*/
-export const agentMode: Mode = {
- name: "agent",
- description: "Direct automation mode for explicit prompts",
-
- shouldTrigger(context) {
- // Only trigger when an explicit prompt is provided
- return !!context.inputs?.prompt;
- },
-
- prepareContext(context) {
- // Agent mode doesn't use comment tracking or branch management
- return {
- mode: "agent",
- githubContext: context,
+export async function prepareAgentMode({
+ context,
+ octokit,
+ githubToken,
+}: {
+ context: GitHubContext;
+ octokit: Octokits;
+ githubToken: string;
+}) {
+ // Check if actor is human (prevents bot-triggered loops)
+ await checkHumanActor(octokit.rest, context);
+
+ // Configure git authentication for agent mode (same as tag mode)
+ // SSH signing takes precedence if provided
+ const useSshSigning = !!context.inputs.sshSigningKey;
+ const useApiCommitSigning = context.inputs.useCommitSigning && !useSshSigning;
+
+ if (useSshSigning) {
+ // Setup SSH signing for commits
+ await setupSshSigning(context.inputs.sshSigningKey);
+
+ // Still configure git auth for push operations (user/email and remote URL)
+ const user = {
+ login: context.inputs.botName,
+ id: parseInt(context.inputs.botId),
};
- },
-
- getAllowedTools() {
- return [];
- },
-
- getDisallowedTools() {
- return [];
- },
-
- shouldCreateTrackingComment() {
- return false;
- },
-
- async prepare({
- context,
- octokit,
- githubToken,
- }: ModeOptions): Promise {
- // Check if actor is human (prevents bot-triggered loops)
- await checkHumanActor(octokit.rest, context);
-
- // Configure git authentication for agent mode (same as tag mode)
- // SSH signing takes precedence if provided
- const useSshSigning = !!context.inputs.sshSigningKey;
- const useApiCommitSigning =
- context.inputs.useCommitSigning && !useSshSigning;
-
- if (useSshSigning) {
- // Setup SSH signing for commits
- await setupSshSigning(context.inputs.sshSigningKey);
-
- // Still configure git auth for push operations (user/email and remote URL)
- const user = {
- login: context.inputs.botName,
- id: parseInt(context.inputs.botId),
- };
- try {
- await configureGitAuth(githubToken, context, user);
- } catch (error) {
- console.error("Failed to configure git authentication:", error);
- // Continue anyway - git operations may still work with default config
- }
- } else if (!useApiCommitSigning) {
- // Use bot_id and bot_name from inputs directly
- const user = {
- login: context.inputs.botName,
- id: parseInt(context.inputs.botId),
- };
-
- try {
- // Use the shared git configuration function
- await configureGitAuth(githubToken, context, user);
- } catch (error) {
- console.error("Failed to configure git authentication:", error);
- // Continue anyway - git operations may still work with default config
- }
+ try {
+ await configureGitAuth(githubToken, context, user);
+ } catch (error) {
+ console.error("Failed to configure git authentication:", error);
+ // Continue anyway - git operations may still work with default config
}
+ } else if (!useApiCommitSigning) {
+ // Use bot_id and bot_name from inputs directly
+ const user = {
+ login: context.inputs.botName,
+ id: parseInt(context.inputs.botId),
+ };
- // Create prompt directory
- await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
- recursive: true,
- });
-
- // Write the prompt file - use the user's prompt directly
- const promptContent =
- context.inputs.prompt ||
- `Repository: ${context.repository.owner}/${context.repository.repo}`;
-
- await writeFile(
- `${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
- promptContent,
- );
-
- // Parse allowed tools from user's claude_args
- const userClaudeArgs = process.env.CLAUDE_ARGS || "";
- const allowedTools = parseAllowedTools(userClaudeArgs);
-
- // Check for branch info from environment variables (useful for auto-fix workflows)
- const claudeBranch = process.env.CLAUDE_BRANCH || undefined;
- const baseBranch =
- process.env.BASE_BRANCH || context.inputs.baseBranch || "main";
-
- // Detect current branch from GitHub environment
- const currentBranch =
- claudeBranch ||
- process.env.GITHUB_HEAD_REF ||
- process.env.GITHUB_REF_NAME ||
- "main";
-
- // Get our GitHub MCP servers config
- const ourMcpConfig = await prepareMcpConfig({
- githubToken,
- owner: context.repository.owner,
- repo: context.repository.repo,
- branch: currentBranch,
- baseBranch: baseBranch,
- claudeCommentId: undefined, // No tracking comment in agent mode
- allowedTools,
- mode: "agent",
- context,
- });
-
- // Build final claude_args with multiple --mcp-config flags
- let claudeArgs = "";
-
- // Add our GitHub servers config if we have any
- const ourConfig = JSON.parse(ourMcpConfig);
- if (ourConfig.mcpServers && Object.keys(ourConfig.mcpServers).length > 0) {
- const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
- claudeArgs = `--mcp-config '${escapedOurConfig}'`;
+ try {
+ // Use the shared git configuration function
+ await configureGitAuth(githubToken, context, user);
+ } catch (error) {
+ console.error("Failed to configure git authentication:", error);
+ // Continue anyway - git operations may still work with default config
}
+ }
- // Append user's claude_args (which may have more --mcp-config flags)
- claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
-
- return {
- commentId: undefined,
- branchInfo: {
- baseBranch: baseBranch,
- currentBranch: baseBranch, // Use base branch as current when creating new branch
- claudeBranch: claudeBranch,
- },
- mcpConfig: ourMcpConfig,
- claudeArgs,
- };
- },
-
- generatePrompt(context: PreparedContext): string {
- // Inject GitHub context as environment variables
- if (context.githubContext) {
- const envVars = extractGitHubContext(context.githubContext);
- for (const [key, value] of Object.entries(envVars)) {
- core.exportVariable(key, value);
- }
- }
+ // Create prompt directory
+ await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
+ recursive: true,
+ });
+
+ // Write the prompt file - use the user's prompt directly
+ const promptContent =
+ context.inputs.prompt ||
+ `Repository: ${context.repository.owner}/${context.repository.repo}`;
+
+ await writeFile(
+ `${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
+ promptContent,
+ );
+
+ // Parse allowed tools from user's claude_args
+ const userClaudeArgs = process.env.CLAUDE_ARGS || "";
+ const allowedTools = parseAllowedTools(userClaudeArgs);
+
+ // Check for branch info from environment variables (useful for auto-fix workflows)
+ const claudeBranch = process.env.CLAUDE_BRANCH || undefined;
+ const baseBranch =
+ process.env.BASE_BRANCH || context.inputs.baseBranch || "main";
+
+ // Detect current branch from GitHub environment
+ const currentBranch =
+ claudeBranch ||
+ process.env.GITHUB_HEAD_REF ||
+ process.env.GITHUB_REF_NAME ||
+ "main";
+
+ // Get our GitHub MCP servers config
+ const ourMcpConfig = await prepareMcpConfig({
+ githubToken,
+ owner: context.repository.owner,
+ repo: context.repository.repo,
+ branch: currentBranch,
+ baseBranch: baseBranch,
+ claudeCommentId: undefined, // No tracking comment in agent mode
+ allowedTools,
+ mode: "agent",
+ context,
+ });
- // Agent mode uses prompt field
- if (context.prompt) {
- return context.prompt;
- }
+ // Build final claude_args with multiple --mcp-config flags
+ let claudeArgs = "";
+
+ // Add our GitHub servers config if we have any
+ const ourConfig = JSON.parse(ourMcpConfig);
+ if (ourConfig.mcpServers && Object.keys(ourConfig.mcpServers).length > 0) {
+ const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
+ claudeArgs = `--mcp-config '${escapedOurConfig}'`;
+ }
- // Minimal fallback - repository is a string in PreparedContext
- return `Repository: ${context.repository}`;
- },
+ // Append user's claude_args (which may have more --mcp-config flags)
+ claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
- getSystemPrompt() {
- // Agent mode doesn't need additional system prompts
- return undefined;
- },
-};
+ return {
+ commentId: undefined,
+ branchInfo: {
+ baseBranch: baseBranch,
+ currentBranch: baseBranch, // Use base branch as current when creating new branch
+ claudeBranch: claudeBranch,
+ },
+ mcpConfig: ourMcpConfig,
+ claudeArgs,
+ };
+}
diff --git a/src/modes/detector.ts b/src/modes/detector.ts
index 8e30aff4f..c15ce88a1 100644
--- a/src/modes/detector.ts
+++ b/src/modes/detector.ts
@@ -80,17 +80,6 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
return "agent";
}
-export function getModeDescription(mode: AutoDetectedMode): string {
- switch (mode) {
- case "tag":
- return "Interactive mode triggered by @claude mentions";
- case "agent":
- return "Direct automation mode for explicit prompts";
- default:
- return "Unknown mode";
- }
-}
-
function validateTrackProgressEvent(context: GitHubContext): void {
// track_progress is only valid for pull_request and issue events
const validEvents = [
@@ -123,21 +112,3 @@ function validateTrackProgressEvent(context: GitHubContext): void {
}
}
}
-
-export function shouldUseTrackingComment(mode: AutoDetectedMode): boolean {
- return mode === "tag";
-}
-
-export function getDefaultPromptForMode(
- mode: AutoDetectedMode,
- context: GitHubContext,
-): string | undefined {
- switch (mode) {
- case "tag":
- return undefined;
- case "agent":
- return context.inputs?.prompt;
- default:
- return undefined;
- }
-}
diff --git a/src/modes/registry.ts b/src/modes/registry.ts
deleted file mode 100644
index 9df69980c..000000000
--- a/src/modes/registry.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Mode Registry for claude-code-action v1.0
- *
- * This module provides access to all available execution modes and handles
- * automatic mode detection based on GitHub event types.
- */
-
-import type { Mode, ModeName } from "./types";
-import { tagMode } from "./tag";
-import { agentMode } from "./agent";
-import type { GitHubContext } from "../github/context";
-import { detectMode, type AutoDetectedMode } from "./detector";
-
-export const VALID_MODES = ["tag", "agent"] as const;
-
-/**
- * All available modes in v1.0
- */
-const modes = {
- tag: tagMode,
- agent: agentMode,
-} as const satisfies Record;
-
-/**
- * Automatically detects and retrieves the appropriate mode based on the GitHub context.
- * In v1.0, modes are auto-selected based on event type.
- * @param context The GitHub context
- * @returns The appropriate mode for the context
- */
-export function getMode(context: GitHubContext): Mode {
- const modeName = detectMode(context);
- console.log(
- `Auto-detected mode: ${modeName} for event: ${context.eventName}`,
- );
-
- const mode = modes[modeName];
- if (!mode) {
- throw new Error(
- `Mode '${modeName}' not found. This should not happen. Please report this issue.`,
- );
- }
-
- return mode;
-}
-
-/**
- * Type guard to check if a string is a valid mode name.
- * @param name The string to check
- * @returns True if the name is a valid mode name
- */
-export function isValidMode(name: string): name is ModeName {
- const validModes = ["tag", "agent"];
- return validModes.includes(name);
-}
diff --git a/src/modes/tag/index.ts b/src/modes/tag/index.ts
index 89f688810..0adf746c2 100644
--- a/src/modes/tag/index.ts
+++ b/src/modes/tag/index.ts
@@ -1,5 +1,3 @@
-import type { Mode, ModeOptions, ModeResult } from "../types";
-import { checkContainsTrigger } from "../../github/validation/trigger";
import { checkHumanActor } from "../../github/validation/actor";
import { createInitialComment } from "../../github/operations/comments/create-initial";
import { setupBranch } from "../../github/operations/branch";
@@ -14,241 +12,177 @@ import {
extractOriginalTitle,
extractOriginalBody,
} from "../../github/data/fetcher";
-import { createPrompt, generateDefaultPrompt } from "../../create-prompt";
+import { createPrompt } from "../../create-prompt";
import { isEntityContext } from "../../github/context";
-import type { PreparedContext } from "../../create-prompt/types";
-import type { FetchDataResult } from "../../github/data/fetcher";
+import type { GitHubContext } from "../../github/context";
+import type { Octokits } from "../../github/api/client";
import { parseAllowedTools } from "../agent/parse-tools";
/**
- * Tag mode implementation.
+ * Prepares the tag mode execution context.
*
- * The traditional implementation mode that responds to @claude mentions,
- * issue assignments, or labels. Creates tracking comments showing progress
- * and has full implementation capabilities.
+ * Tag mode responds to @claude mentions, issue assignments, or labels.
+ * Creates tracking comments showing progress and has full implementation capabilities.
*/
-export const tagMode: Mode = {
- name: "tag",
- description: "Traditional implementation mode triggered by @claude mentions",
-
- shouldTrigger(context) {
- // Tag mode only handles entity events
- if (!isEntityContext(context)) {
- return false;
- }
- return checkContainsTrigger(context);
- },
-
- prepareContext(context, data) {
- return {
- mode: "tag",
- githubContext: context,
- commentId: data?.commentId,
- baseBranch: data?.baseBranch,
- claudeBranch: data?.claudeBranch,
+export async function prepareTagMode({
+ context,
+ octokit,
+ githubToken,
+}: {
+ context: GitHubContext;
+ octokit: Octokits;
+ githubToken: string;
+}) {
+ // Tag mode only handles entity-based events
+ if (!isEntityContext(context)) {
+ throw new Error("Tag mode requires entity context");
+ }
+
+ // Check if actor is human
+ await checkHumanActor(octokit.rest, context);
+
+ // Create initial tracking comment
+ const commentData = await createInitialComment(octokit.rest, context);
+ const commentId = commentData.id;
+
+ const triggerTime = extractTriggerTimestamp(context);
+ const originalTitle = extractOriginalTitle(context);
+ const originalBody = extractOriginalBody(context);
+
+ const githubData = await fetchGitHubData({
+ octokits: octokit,
+ repository: `${context.repository.owner}/${context.repository.repo}`,
+ prNumber: context.entityNumber.toString(),
+ isPR: context.isPR,
+ triggerUsername: context.actor,
+ triggerTime,
+ originalTitle,
+ originalBody,
+ includeCommentsByActor: context.inputs.includeCommentsByActor,
+ excludeCommentsByActor: context.inputs.excludeCommentsByActor,
+ });
+
+ // Setup branch
+ const branchInfo = await setupBranch(octokit, githubData, context);
+
+ // Configure git authentication
+ // SSH signing takes precedence if provided
+ const useSshSigning = !!context.inputs.sshSigningKey;
+ const useApiCommitSigning = context.inputs.useCommitSigning && !useSshSigning;
+
+ if (useSshSigning) {
+ // Setup SSH signing for commits
+ await setupSshSigning(context.inputs.sshSigningKey);
+
+ // Still configure git auth for push operations (user/email and remote URL)
+ const user = {
+ login: context.inputs.botName,
+ id: parseInt(context.inputs.botId),
};
- },
-
- getAllowedTools() {
- return [];
- },
-
- getDisallowedTools() {
- return [];
- },
-
- shouldCreateTrackingComment() {
- return true;
- },
-
- async prepare({
- context,
- octokit,
- githubToken,
- }: ModeOptions): Promise {
- // Tag mode only handles entity-based events
- if (!isEntityContext(context)) {
- throw new Error("Tag mode requires entity context");
+ try {
+ await configureGitAuth(githubToken, context, user);
+ } catch (error) {
+ console.error("Failed to configure git authentication:", error);
+ throw error;
}
+ } else if (!useApiCommitSigning) {
+ // Use bot_id and bot_name from inputs directly
+ const user = {
+ login: context.inputs.botName,
+ id: parseInt(context.inputs.botId),
+ };
- // Check if actor is human
- await checkHumanActor(octokit.rest, context);
-
- // Create initial tracking comment
- const commentData = await createInitialComment(octokit.rest, context);
- const commentId = commentData.id;
-
- const triggerTime = extractTriggerTimestamp(context);
- const originalTitle = extractOriginalTitle(context);
- const originalBody = extractOriginalBody(context);
-
- const githubData = await fetchGitHubData({
- octokits: octokit,
- repository: `${context.repository.owner}/${context.repository.repo}`,
- prNumber: context.entityNumber.toString(),
- isPR: context.isPR,
- triggerUsername: context.actor,
- triggerTime,
- originalTitle,
- originalBody,
- includeCommentsByActor: context.inputs.includeCommentsByActor,
- excludeCommentsByActor: context.inputs.excludeCommentsByActor,
- });
-
- // Setup branch
- const branchInfo = await setupBranch(octokit, githubData, context);
-
- // Configure git authentication
- // SSH signing takes precedence if provided
- const useSshSigning = !!context.inputs.sshSigningKey;
- const useApiCommitSigning =
- context.inputs.useCommitSigning && !useSshSigning;
-
- if (useSshSigning) {
- // Setup SSH signing for commits
- await setupSshSigning(context.inputs.sshSigningKey);
-
- // Still configure git auth for push operations (user/email and remote URL)
- const user = {
- login: context.inputs.botName,
- id: parseInt(context.inputs.botId),
- };
- try {
- await configureGitAuth(githubToken, context, user);
- } catch (error) {
- console.error("Failed to configure git authentication:", error);
- throw error;
- }
- } else if (!useApiCommitSigning) {
- // Use bot_id and bot_name from inputs directly
- const user = {
- login: context.inputs.botName,
- id: parseInt(context.inputs.botId),
- };
-
- try {
- await configureGitAuth(githubToken, context, user);
- } catch (error) {
- console.error("Failed to configure git authentication:", error);
- throw error;
- }
+ try {
+ await configureGitAuth(githubToken, context, user);
+ } catch (error) {
+ console.error("Failed to configure git authentication:", error);
+ throw error;
}
-
- // Create prompt file
- const modeContext = this.prepareContext(context, {
- commentId,
- baseBranch: branchInfo.baseBranch,
- claudeBranch: branchInfo.claudeBranch,
- });
-
- await createPrompt(tagMode, modeContext, githubData, context);
-
- const userClaudeArgs = process.env.CLAUDE_ARGS || "";
- const userAllowedMCPTools = parseAllowedTools(userClaudeArgs).filter(
- (tool) => tool.startsWith("mcp__github_"),
+ }
+
+ // Create prompt file
+ await createPrompt(
+ commentId,
+ branchInfo.baseBranch,
+ branchInfo.claudeBranch,
+ githubData,
+ context,
+ );
+
+ const userClaudeArgs = process.env.CLAUDE_ARGS || "";
+ const userAllowedMCPTools = parseAllowedTools(userClaudeArgs).filter((tool) =>
+ tool.startsWith("mcp__github_"),
+ );
+
+ // Build claude_args for tag mode with required tools
+ // Tag mode REQUIRES these tools to function properly
+ const tagModeTools = [
+ "Edit",
+ "MultiEdit",
+ "Glob",
+ "Grep",
+ "LS",
+ "Read",
+ "Write",
+ "mcp__github_comment__update_claude_comment",
+ "mcp__github_ci__get_ci_status",
+ "mcp__github_ci__get_workflow_run_details",
+ "mcp__github_ci__download_job_log",
+ ...userAllowedMCPTools,
+ ];
+
+ // Add git commands when using git CLI (no API commit signing, or SSH signing)
+ // SSH signing still uses git CLI, just with signing enabled
+ if (!useApiCommitSigning) {
+ tagModeTools.push(
+ "Bash(git add:*)",
+ "Bash(git commit:*)",
+ "Bash(git push:*)",
+ "Bash(git status:*)",
+ "Bash(git diff:*)",
+ "Bash(git log:*)",
+ "Bash(git rm:*)",
);
-
- // Build claude_args for tag mode with required tools
- // Tag mode REQUIRES these tools to function properly
- const tagModeTools = [
- "Edit",
- "MultiEdit",
- "Glob",
- "Grep",
- "LS",
- "Read",
- "Write",
- "mcp__github_comment__update_claude_comment",
- "mcp__github_ci__get_ci_status",
- "mcp__github_ci__get_workflow_run_details",
- "mcp__github_ci__download_job_log",
- ...userAllowedMCPTools,
- ];
-
- // Add git commands when using git CLI (no API commit signing, or SSH signing)
- // SSH signing still uses git CLI, just with signing enabled
- if (!useApiCommitSigning) {
- tagModeTools.push(
- "Bash(git add:*)",
- "Bash(git commit:*)",
- "Bash(git push:*)",
- "Bash(git status:*)",
- "Bash(git diff:*)",
- "Bash(git log:*)",
- "Bash(git rm:*)",
- );
- } else {
- // When using API commit signing, use MCP file ops tools
- tagModeTools.push(
- "mcp__github_file_ops__commit_files",
- "mcp__github_file_ops__delete_files",
- );
- }
-
- // Get our GitHub MCP servers configuration
- const ourMcpConfig = await prepareMcpConfig({
- githubToken,
- owner: context.repository.owner,
- repo: context.repository.repo,
- branch: branchInfo.claudeBranch || branchInfo.currentBranch,
- baseBranch: branchInfo.baseBranch,
- claudeCommentId: commentId.toString(),
- allowedTools: Array.from(new Set(tagModeTools)),
- mode: "tag",
- context,
- });
-
- // Build complete claude_args with multiple --mcp-config flags
- let claudeArgs = "";
-
- // Add our GitHub servers config
- const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
- claudeArgs = `--mcp-config '${escapedOurConfig}'`;
-
- // Add required tools for tag mode
- claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
-
- // Append user's claude_args (which may have more --mcp-config flags)
- if (userClaudeArgs) {
- claudeArgs += ` ${userClaudeArgs}`;
- }
-
- return {
- commentId,
- branchInfo,
- mcpConfig: ourMcpConfig,
- claudeArgs: claudeArgs.trim(),
- };
- },
-
- generatePrompt(
- context: PreparedContext,
- githubData: FetchDataResult,
- useCommitSigning: boolean,
- ): string {
- const defaultPrompt = generateDefaultPrompt(
- context,
- githubData,
- useCommitSigning,
+ } else {
+ // When using API commit signing, use MCP file ops tools
+ tagModeTools.push(
+ "mcp__github_file_ops__commit_files",
+ "mcp__github_file_ops__delete_files",
);
+ }
- // If a custom prompt is provided, inject it into the tag mode prompt
- if (context.githubContext?.inputs?.prompt) {
- return (
- defaultPrompt +
- `
-
-
-${context.githubContext.inputs.prompt}
-`
- );
- }
-
- return defaultPrompt;
- },
-
- getSystemPrompt() {
- // Tag mode doesn't need additional system prompts
- return undefined;
- },
-};
+ // Get our GitHub MCP servers configuration
+ const ourMcpConfig = await prepareMcpConfig({
+ githubToken,
+ owner: context.repository.owner,
+ repo: context.repository.repo,
+ branch: branchInfo.claudeBranch || branchInfo.currentBranch,
+ baseBranch: branchInfo.baseBranch,
+ claudeCommentId: commentId.toString(),
+ allowedTools: Array.from(new Set(tagModeTools)),
+ mode: "tag",
+ context,
+ });
+
+ // Build complete claude_args with multiple --mcp-config flags
+ let claudeArgs = "";
+
+ // Add our GitHub servers config
+ const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
+ claudeArgs = `--mcp-config '${escapedOurConfig}'`;
+
+ // Add required tools for tag mode
+ claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
+
+ // Append user's claude_args (which may have more --mcp-config flags)
+ if (userClaudeArgs) {
+ claudeArgs += ` ${userClaudeArgs}`;
+ }
+
+ return {
+ commentId,
+ branchInfo,
+ mcpConfig: ourMcpConfig,
+ claudeArgs: claudeArgs.trim(),
+ };
+}
diff --git a/src/modes/types.ts b/src/modes/types.ts
deleted file mode 100644
index 3d653ccd3..000000000
--- a/src/modes/types.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import type { GitHubContext } from "../github/context";
-import type { PreparedContext } from "../create-prompt/types";
-import type { FetchDataResult } from "../github/data/fetcher";
-import type { Octokits } from "../github/api/client";
-
-export type ModeName = "tag" | "agent";
-
-export type ModeContext = {
- mode: ModeName;
- githubContext: GitHubContext;
- commentId?: number;
- baseBranch?: string;
- claudeBranch?: string;
-};
-
-export type ModeData = {
- commentId?: number;
- baseBranch?: string;
- claudeBranch?: string;
-};
-
-/**
- * Mode interface for claude-code-action execution modes.
- * Each mode defines its own behavior for trigger detection, prompt generation,
- * and tracking comment creation.
- *
- * Current modes include:
- * - 'tag': Interactive mode triggered by @claude mentions
- * - 'agent': Direct automation mode triggered by explicit prompts
- */
-export type Mode = {
- name: ModeName;
- description: string;
-
- /**
- * Determines if this mode should trigger based on the GitHub context
- */
- shouldTrigger(context: GitHubContext): boolean;
-
- /**
- * Prepares the mode context with any additional data needed for prompt generation
- */
- prepareContext(context: GitHubContext, data?: ModeData): ModeContext;
-
- /**
- * Returns the list of tools that should be allowed for this mode
- */
- getAllowedTools(): string[];
-
- /**
- * Returns the list of tools that should be disallowed for this mode
- */
- getDisallowedTools(): string[];
-
- /**
- * Determines if this mode should create a tracking comment
- */
- shouldCreateTrackingComment(): boolean;
-
- /**
- * Generates the prompt for this mode.
- * @returns The complete prompt string
- */
- generatePrompt(
- context: PreparedContext,
- githubData: FetchDataResult,
- useCommitSigning: boolean,
- ): string;
-
- /**
- * Prepares the GitHub environment for this mode.
- * Each mode decides how to handle different event types.
- * @returns PrepareResult with commentId, branchInfo, and mcpConfig
- */
- prepare(options: ModeOptions): Promise;
-
- /**
- * Returns an optional system prompt to append to Claude's base system prompt.
- * This allows modes to add mode-specific instructions.
- * @returns The system prompt string or undefined if no additional prompt is needed
- */
- getSystemPrompt?(context: ModeContext): string | undefined;
-};
-
-// Define types for mode prepare method
-export type ModeOptions = {
- context: GitHubContext;
- octokit: Octokits;
- githubToken: string;
-};
-
-export type ModeResult = {
- commentId?: number;
- branchInfo: {
- baseBranch: string;
- claudeBranch?: string;
- currentBranch: string;
- };
- mcpConfig: string;
- claudeArgs: string;
-};
diff --git a/src/prepare/index.ts b/src/prepare/index.ts
deleted file mode 100644
index 6f4230192..000000000
--- a/src/prepare/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Main prepare module that delegates to the mode's prepare method
- */
-
-import type { PrepareOptions, PrepareResult } from "./types";
-
-export async function prepare(options: PrepareOptions): Promise {
- const { mode, context, octokit, githubToken } = options;
-
- console.log(
- `Preparing with mode: ${mode.name} for event: ${context.eventName}`,
- );
-
- // Delegate to the mode's prepare method
- return mode.prepare({
- context,
- octokit,
- githubToken,
- });
-}
diff --git a/src/prepare/types.ts b/src/prepare/types.ts
deleted file mode 100644
index 72ece515b..000000000
--- a/src/prepare/types.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { GitHubContext } from "../github/context";
-import type { Octokits } from "../github/api/client";
-import type { Mode } from "../modes/types";
-
-export type PrepareResult = {
- commentId?: number;
- branchInfo: {
- baseBranch: string;
- claudeBranch?: string;
- currentBranch: string;
- };
- mcpConfig: string;
- claudeArgs: string;
-};
-
-export type PrepareOptions = {
- context: GitHubContext;
- octokit: Octokits;
- mode: Mode;
- githubToken: string;
-};
diff --git a/test/create-prompt.test.ts b/test/create-prompt.test.ts
index ca2468175..0a5ea8253 100644
--- a/test/create-prompt.test.ts
+++ b/test/create-prompt.test.ts
@@ -3,60 +3,13 @@
import { describe, test, expect } from "bun:test";
import {
generatePrompt,
- generateDefaultPrompt,
getEventTypeAndContext,
buildAllowedToolsString,
buildDisallowedToolsString,
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
-import type { Mode } from "../src/modes/types";
describe("generatePrompt", () => {
- // Create a mock tag mode that uses the default prompt
- const mockTagMode: Mode = {
- name: "tag",
- description: "Tag mode",
- shouldTrigger: () => true,
- prepareContext: (context) => ({ mode: "tag", githubContext: context }),
- getAllowedTools: () => [],
- getDisallowedTools: () => [],
- shouldCreateTrackingComment: () => true,
- generatePrompt: (context, githubData, useCommitSigning) =>
- generateDefaultPrompt(context, githubData, useCommitSigning),
- prepare: async () => ({
- commentId: 123,
- branchInfo: {
- baseBranch: "main",
- currentBranch: "main",
- claudeBranch: undefined,
- },
- mcpConfig: "{}",
- claudeArgs: "",
- }),
- };
-
- // Create a mock agent mode that passes through prompts
- const mockAgentMode: Mode = {
- name: "agent",
- description: "Agent mode",
- shouldTrigger: () => true,
- prepareContext: (context) => ({ mode: "agent", githubContext: context }),
- getAllowedTools: () => [],
- getDisallowedTools: () => [],
- shouldCreateTrackingComment: () => false,
- generatePrompt: (context) => context.prompt || "",
- prepare: async () => ({
- commentId: undefined,
- branchInfo: {
- baseBranch: "main",
- currentBranch: "main",
- claudeBranch: undefined,
- },
- mcpConfig: "{}",
- claudeArgs: "",
- }),
- };
-
const mockGitHubData = {
contextData: {
title: "Test PR",
@@ -181,12 +134,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("GENERAL_COMMENT");
@@ -214,12 +162,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
expect(prompt).toContain("PR_REVIEW");
expect(prompt).toContain("true");
@@ -245,12 +188,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
expect(prompt).toContain("ISSUE_CREATED");
expect(prompt).toContain(
@@ -278,12 +216,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
expect(prompt).toContain("ISSUE_ASSIGNED");
expect(prompt).toContain(
@@ -310,12 +243,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
expect(prompt).toContain("ISSUE_LABELED");
expect(prompt).toContain(
@@ -341,12 +269,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
expect(prompt).toContain("PULL_REQUEST");
expect(prompt).toContain("true");
@@ -370,12 +293,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Verify prompt generates successfully without custom instructions
expect(prompt).toContain("@claude please fix this");
@@ -400,7 +318,7 @@ describe("generatePrompt", () => {
envVars,
mockGitHubData,
false,
- mockAgentMode,
+ "agent",
);
// Agent mode: Prompt is passed through as-is
@@ -441,7 +359,7 @@ describe("generatePrompt", () => {
envVars,
mockGitHubData,
false,
- mockAgentMode,
+ "agent",
);
// v1.0: Variables are NOT substituted - prompt is passed as-is to Claude Code
@@ -490,7 +408,7 @@ describe("generatePrompt", () => {
envVars,
issueGitHubData,
false,
- mockAgentMode,
+ "agent",
);
// Agent mode: Prompt is passed through as-is
@@ -515,7 +433,7 @@ describe("generatePrompt", () => {
envVars,
mockGitHubData,
false,
- mockAgentMode,
+ "agent",
);
// Agent mode: No substitution - passed as-is
@@ -539,12 +457,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("ISSUE_CREATED");
@@ -567,12 +480,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
expect(prompt).toContain("johndoe");
// With commit signing disabled, co-author info appears in git commit instructions
@@ -594,12 +502,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain PR-specific instructions (git commands when not using signing)
expect(prompt).toContain("git push");
@@ -630,12 +533,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain Issue-specific instructions
expect(prompt).toContain(
@@ -674,12 +572,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain the actual branch name with timestamp
expect(prompt).toContain(
@@ -709,12 +602,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain branch-specific instructions like issues
expect(prompt).toContain(
@@ -752,12 +640,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain open PR instructions (git commands when not using signing)
expect(prompt).toContain("git push");
@@ -788,12 +671,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain new branch instructions
expect(prompt).toContain(
@@ -821,12 +699,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain new branch instructions
expect(prompt).toContain(
@@ -854,12 +727,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain new branch instructions
expect(prompt).toContain(
@@ -883,12 +751,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should have git command instructions
expect(prompt).toContain("Use git commands via the Bash tool");
@@ -917,12 +780,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = await generatePrompt(
- envVars,
- mockGitHubData,
- true,
- mockTagMode,
- );
+ const prompt = await generatePrompt(envVars, mockGitHubData, true, "tag");
// Should have commit signing tool instructions
expect(prompt).toContain("mcp__github_file_ops__commit_files");
diff --git a/test/modes/agent.test.ts b/test/modes/agent.test.ts
index b7224069b..be6f0d2b1 100644
--- a/test/modes/agent.test.ts
+++ b/test/modes/agent.test.ts
@@ -7,22 +7,17 @@ import {
spyOn,
mock,
} from "bun:test";
-import { agentMode } from "../../src/modes/agent";
-import type { GitHubContext } from "../../src/github/context";
-import { createMockContext, createMockAutomationContext } from "../mockContext";
+import { prepareAgentMode } from "../../src/modes/agent";
+import { createMockAutomationContext } from "../mockContext";
import * as core from "@actions/core";
import * as gitConfig from "../../src/github/operations/git-config";
describe("Agent Mode", () => {
- let mockContext: GitHubContext;
let exportVariableSpy: any;
let setOutputSpy: any;
let configureGitAuthSpy: any;
beforeEach(() => {
- mockContext = createMockAutomationContext({
- eventName: "workflow_dispatch",
- });
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
() => {},
);
@@ -45,84 +40,11 @@ describe("Agent Mode", () => {
configureGitAuthSpy?.mockRestore();
});
- test("agent mode has correct properties", () => {
- expect(agentMode.name).toBe("agent");
- expect(agentMode.description).toBe(
- "Direct automation mode for explicit prompts",
- );
- expect(agentMode.shouldCreateTrackingComment()).toBe(false);
- expect(agentMode.getAllowedTools()).toEqual([]);
- expect(agentMode.getDisallowedTools()).toEqual([]);
- });
-
- test("prepareContext returns minimal data", () => {
- const context = agentMode.prepareContext(mockContext);
-
- expect(context.mode).toBe("agent");
- expect(context.githubContext).toBe(mockContext);
- // Agent mode doesn't use comment tracking or branch management
- expect(Object.keys(context)).toEqual(["mode", "githubContext"]);
- });
-
- test("agent mode only triggers when prompt is provided", () => {
- // Should NOT trigger for automation events without prompt
- const workflowDispatchContext = createMockAutomationContext({
- eventName: "workflow_dispatch",
- });
- expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(false);
-
- const scheduleContext = createMockAutomationContext({
- eventName: "schedule",
- });
- expect(agentMode.shouldTrigger(scheduleContext)).toBe(false);
-
- const repositoryDispatchContext = createMockAutomationContext({
- eventName: "repository_dispatch",
- });
- expect(agentMode.shouldTrigger(repositoryDispatchContext)).toBe(false);
-
- // Should NOT trigger for entity events without prompt
- const entityEvents = [
- "issue_comment",
- "pull_request",
- "pull_request_review",
- "issues",
- ] as const;
-
- entityEvents.forEach((eventName) => {
- const contextNoPrompt = createMockContext({ eventName });
- expect(agentMode.shouldTrigger(contextNoPrompt)).toBe(false);
- });
-
- // Should trigger for ANY event when prompt is provided
- const allEvents = [
- "workflow_dispatch",
- "repository_dispatch",
- "schedule",
- "issue_comment",
- "pull_request",
- "pull_request_review",
- "issues",
- ] as const;
-
- allEvents.forEach((eventName) => {
- const contextWithPrompt =
- eventName === "workflow_dispatch" ||
- eventName === "repository_dispatch" ||
- eventName === "schedule"
- ? createMockAutomationContext({
- eventName,
- inputs: { prompt: "Do something" },
- })
- : createMockContext({
- eventName,
- inputs: { prompt: "Do something" },
- });
- expect(agentMode.shouldTrigger(contextWithPrompt)).toBe(true);
- });
+ test("prepareAgentMode is exported as a function", () => {
+ expect(typeof prepareAgentMode).toBe("function");
});
- test("prepare method passes through claude_args", async () => {
+ test("prepare passes through claude_args", async () => {
// Clear any previous calls before this test
exportVariableSpy.mockClear();
setOutputSpy.mockClear();
@@ -156,7 +78,7 @@ describe("Agent Mode", () => {
},
},
} as any;
- const result = await agentMode.prepare({
+ const result = await prepareAgentMode({
context: contextWithCustomArgs,
octokit: mockOctokit,
githubToken: "test-token",
@@ -186,7 +108,7 @@ describe("Agent Mode", () => {
process.env.GITHUB_REF_NAME = originalRefName;
});
- test("prepare method rejects bot actors without allowed_bots", async () => {
+ test("prepare rejects bot actors without allowed_bots", async () => {
const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch",
});
@@ -206,7 +128,7 @@ describe("Agent Mode", () => {
} as any;
await expect(
- agentMode.prepare({
+ prepareAgentMode({
context: contextWithPrompts,
octokit: mockOctokit,
githubToken: "test-token",
@@ -216,7 +138,7 @@ describe("Agent Mode", () => {
);
});
- test("prepare method allows bot actors when in allowed_bots list", async () => {
+ test("prepare allows bot actors when in allowed_bots list", async () => {
const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch",
});
@@ -237,7 +159,7 @@ describe("Agent Mode", () => {
// Should not throw - bot is in allowed list
await expect(
- agentMode.prepare({
+ prepareAgentMode({
context: contextWithPrompts,
octokit: mockOctokit,
githubToken: "test-token",
@@ -245,7 +167,7 @@ describe("Agent Mode", () => {
).resolves.toBeDefined();
});
- test("prepare method creates prompt file with correct content", async () => {
+ test("prepare creates prompt file with correct content", async () => {
const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch",
});
@@ -268,7 +190,7 @@ describe("Agent Mode", () => {
},
},
} as any;
- const result = await agentMode.prepare({
+ const result = await prepareAgentMode({
context: contextWithPrompts,
octokit: mockOctokit,
githubToken: "test-token",
diff --git a/test/modes/registry.test.ts b/test/modes/registry.test.ts
deleted file mode 100644
index 7c585b27f..000000000
--- a/test/modes/registry.test.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import { describe, test, expect } from "bun:test";
-import { getMode, isValidMode } from "../../src/modes/registry";
-import { agentMode } from "../../src/modes/agent";
-import { tagMode } from "../../src/modes/tag";
-import {
- createMockContext,
- createMockAutomationContext,
- mockRepositoryDispatchContext,
-} from "../mockContext";
-
-describe("Mode Registry", () => {
- const mockContext = createMockContext({
- eventName: "issue_comment",
- payload: {
- action: "created",
- comment: {
- body: "Test comment without trigger",
- },
- } as any,
- });
-
- const mockWorkflowDispatchContext = createMockAutomationContext({
- eventName: "workflow_dispatch",
- });
-
- const mockScheduleContext = createMockAutomationContext({
- eventName: "schedule",
- });
-
- test("getMode auto-detects agent mode for issue_comment without trigger", () => {
- const mode = getMode(mockContext);
- // Agent mode is the default when no trigger is found
- expect(mode).toBe(agentMode);
- expect(mode.name).toBe("agent");
- });
-
- test("getMode auto-detects agent mode for workflow_dispatch", () => {
- const mode = getMode(mockWorkflowDispatchContext);
- expect(mode).toBe(agentMode);
- expect(mode.name).toBe("agent");
- });
-
- // Removed test - explicit mode override no longer supported in v1.0
-
- test("getMode auto-detects agent for workflow_dispatch", () => {
- const mode = getMode(mockWorkflowDispatchContext);
- expect(mode).toBe(agentMode);
- expect(mode.name).toBe("agent");
- });
-
- test("getMode auto-detects agent for schedule event", () => {
- const mode = getMode(mockScheduleContext);
- expect(mode).toBe(agentMode);
- expect(mode.name).toBe("agent");
- });
-
- test("getMode auto-detects agent for repository_dispatch event", () => {
- const mode = getMode(mockRepositoryDispatchContext);
- expect(mode).toBe(agentMode);
- expect(mode.name).toBe("agent");
- });
-
- test("getMode auto-detects agent for repository_dispatch with client_payload", () => {
- const contextWithPayload = createMockAutomationContext({
- eventName: "repository_dispatch",
- payload: {
- action: "trigger-analysis",
- client_payload: {
- source: "external-system",
- metadata: { priority: "high" },
- },
- repository: {
- name: "test-repo",
- owner: { login: "test-owner" },
- },
- sender: { login: "automation-user" },
- },
- });
-
- const mode = getMode(contextWithPayload);
- expect(mode).toBe(agentMode);
- expect(mode.name).toBe("agent");
- });
-
- // Removed test - legacy mode names no longer supported in v1.0
-
- test("getMode auto-detects agent mode for PR opened", () => {
- const prContext = createMockContext({
- eventName: "pull_request",
- payload: { action: "opened" } as any,
- isPR: true,
- });
- const mode = getMode(prContext);
- expect(mode).toBe(agentMode);
- expect(mode.name).toBe("agent");
- });
-
- test("getMode uses agent mode when prompt is provided, even with @claude mention", () => {
- const contextWithPrompt = createMockContext({
- eventName: "issue_comment",
- payload: {
- action: "created",
- comment: {
- body: "@claude please help",
- },
- } as any,
- inputs: {
- prompt: "/review",
- } as any,
- });
- const mode = getMode(contextWithPrompt);
- expect(mode).toBe(agentMode);
- expect(mode.name).toBe("agent");
- });
-
- test("getMode uses tag mode for @claude mention without prompt", () => {
- // Ensure PROMPT env var is not set (clean up from previous tests)
- const originalPrompt = process.env.PROMPT;
- delete process.env.PROMPT;
-
- const contextWithMention = createMockContext({
- eventName: "issue_comment",
- payload: {
- action: "created",
- comment: {
- body: "@claude please help",
- },
- } as any,
- inputs: {
- triggerPhrase: "@claude",
- prompt: "",
- } as any,
- });
- const mode = getMode(contextWithMention);
- expect(mode).toBe(tagMode);
- expect(mode.name).toBe("tag");
-
- // Restore original value if it existed
- if (originalPrompt !== undefined) {
- process.env.PROMPT = originalPrompt;
- }
- });
-
- // Removed test - explicit mode override no longer supported in v1.0
-
- test("isValidMode returns true for all valid modes", () => {
- expect(isValidMode("tag")).toBe(true);
- expect(isValidMode("agent")).toBe(true);
- });
-
- test("isValidMode returns false for invalid mode", () => {
- expect(isValidMode("invalid")).toBe(false);
- expect(isValidMode("review")).toBe(false);
- });
-});
diff --git a/test/modes/tag.test.ts b/test/modes/tag.test.ts
index d592463f5..d68d7fc20 100644
--- a/test/modes/tag.test.ts
+++ b/test/modes/tag.test.ts
@@ -1,92 +1,8 @@
-import { describe, test, expect, beforeEach } from "bun:test";
-import { tagMode } from "../../src/modes/tag";
-import type { ParsedGitHubContext } from "../../src/github/context";
-import type { IssueCommentEvent } from "@octokit/webhooks-types";
-import { createMockContext } from "../mockContext";
+import { describe, test, expect } from "bun:test";
+import { prepareTagMode } from "../../src/modes/tag";
describe("Tag Mode", () => {
- let mockContext: ParsedGitHubContext;
-
- beforeEach(() => {
- mockContext = createMockContext({
- eventName: "issue_comment",
- isPR: false,
- });
- });
-
- test("tag mode has correct properties", () => {
- expect(tagMode.name).toBe("tag");
- expect(tagMode.description).toBe(
- "Traditional implementation mode triggered by @claude mentions",
- );
- expect(tagMode.shouldCreateTrackingComment()).toBe(true);
- });
-
- test("shouldTrigger delegates to checkContainsTrigger", () => {
- const contextWithTrigger = createMockContext({
- eventName: "issue_comment",
- isPR: false,
- inputs: {
- ...createMockContext().inputs,
- triggerPhrase: "@claude",
- },
- payload: {
- comment: {
- body: "Hey @claude, can you help?",
- },
- } as IssueCommentEvent,
- });
-
- expect(tagMode.shouldTrigger(contextWithTrigger)).toBe(true);
-
- const contextWithoutTrigger = createMockContext({
- eventName: "issue_comment",
- isPR: false,
- inputs: {
- ...createMockContext().inputs,
- triggerPhrase: "@claude",
- },
- payload: {
- comment: {
- body: "This is just a regular comment",
- },
- } as IssueCommentEvent,
- });
-
- expect(tagMode.shouldTrigger(contextWithoutTrigger)).toBe(false);
- });
-
- test("prepareContext includes all required data", () => {
- const data = {
- commentId: 123,
- baseBranch: "main",
- claudeBranch: "claude/fix-bug",
- };
-
- const context = tagMode.prepareContext(mockContext, data);
-
- expect(context.mode).toBe("tag");
- expect(context.githubContext).toBe(mockContext);
- expect(context.commentId).toBe(123);
- expect(context.baseBranch).toBe("main");
- expect(context.claudeBranch).toBe("claude/fix-bug");
- });
-
- test("prepareContext works without data", () => {
- const context = tagMode.prepareContext(mockContext);
-
- expect(context.mode).toBe("tag");
- expect(context.githubContext).toBe(mockContext);
- expect(context.commentId).toBeUndefined();
- expect(context.baseBranch).toBeUndefined();
- expect(context.claudeBranch).toBeUndefined();
- });
-
- test("getAllowedTools returns empty array", () => {
- expect(tagMode.getAllowedTools()).toEqual([]);
- });
-
- test("getDisallowedTools returns empty array", () => {
- expect(tagMode.getDisallowedTools()).toEqual([]);
+ test("prepareTagMode is exported as a function", () => {
+ expect(typeof prepareTagMode).toBe("function");
});
});
diff --git a/test/pull-request-target.test.ts b/test/pull-request-target.test.ts
index 7f4fd342d..59c747f28 100644
--- a/test/pull-request-target.test.ts
+++ b/test/pull-request-target.test.ts
@@ -1,38 +1,10 @@
#!/usr/bin/env bun
import { describe, test, expect } from "bun:test";
-import {
- getEventTypeAndContext,
- generatePrompt,
- generateDefaultPrompt,
-} from "../src/create-prompt";
+import { getEventTypeAndContext, generatePrompt } from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
-import type { Mode } from "../src/modes/types";
describe("pull_request_target event support", () => {
- // Mock tag mode for testing
- const mockTagMode: Mode = {
- name: "tag",
- description: "Tag mode",
- shouldTrigger: () => true,
- prepareContext: (context) => ({ mode: "tag", githubContext: context }),
- getAllowedTools: () => [],
- getDisallowedTools: () => [],
- shouldCreateTrackingComment: () => true,
- generatePrompt: (context, githubData, useCommitSigning) =>
- generateDefaultPrompt(context, githubData, useCommitSigning),
- prepare: async () => ({
- commentId: 123,
- branchInfo: {
- baseBranch: "main",
- currentBranch: "main",
- claudeBranch: undefined,
- },
- mcpConfig: "{}",
- claudeArgs: "",
- }),
- };
-
const mockGitHubData = {
contextData: {
title: "External PR via pull_request_target",
@@ -126,12 +98,7 @@ describe("pull_request_target event support", () => {
},
};
- const prompt = generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain pull request event type and metadata
expect(prompt).toContain("PULL_REQUEST");
@@ -165,12 +132,7 @@ describe("pull_request_target event support", () => {
},
};
- const prompt = generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
// Should include git commands for non-commit-signing mode
expect(prompt).toContain("git push");
@@ -196,7 +158,7 @@ describe("pull_request_target event support", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData, true, mockTagMode);
+ const prompt = generatePrompt(envVars, mockGitHubData, true, "tag");
// Should include commit signing tools
expect(prompt).toContain("mcp__github_file_ops__commit_files");
@@ -246,13 +208,13 @@ describe("pull_request_target event support", () => {
pullRequestContext,
mockGitHubData,
false,
- mockTagMode,
+ "tag",
);
const pullRequestTargetPrompt = generatePrompt(
pullRequestTargetContext,
mockGitHubData,
false,
- mockTagMode,
+ "tag",
);
// Both should have the same event type and structure
@@ -293,37 +255,7 @@ describe("pull_request_target event support", () => {
},
};
- // Use agent mode which passes through the prompt as-is
- const mockAgentMode: Mode = {
- name: "agent",
- description: "Agent mode",
- shouldTrigger: () => true,
- prepareContext: (context) => ({
- mode: "agent",
- githubContext: context,
- }),
- getAllowedTools: () => [],
- getDisallowedTools: () => [],
- shouldCreateTrackingComment: () => true,
- generatePrompt: (context) => context.prompt || "default prompt",
- prepare: async () => ({
- commentId: 123,
- branchInfo: {
- baseBranch: "main",
- currentBranch: "main",
- claudeBranch: undefined,
- },
- mcpConfig: "{}",
- claudeArgs: "",
- }),
- };
-
- const prompt = generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockAgentMode,
- );
+ const prompt = generatePrompt(envVars, mockGitHubData, false, "agent");
expect(prompt).toBe(
"Review this pull_request_target PR for security issues",
@@ -343,12 +275,7 @@ describe("pull_request_target event support", () => {
},
};
- const prompt = generatePrompt(
- envVars,
- mockGitHubData,
- false,
- mockTagMode,
- );
+ const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
// Should generate default prompt structure
expect(prompt).toContain("PULL_REQUEST");
@@ -418,7 +345,7 @@ describe("pull_request_target event support", () => {
// Should not throw when generating prompt
expect(() => {
- generatePrompt(minimalContext, mockGitHubData, false, mockTagMode);
+ generatePrompt(minimalContext, mockGitHubData, false, "tag");
}).not.toThrow();
});
@@ -476,13 +403,13 @@ describe("pull_request_target event support", () => {
internalPR,
mockGitHubData,
false,
- mockTagMode,
+ "tag",
);
const externalPrompt = generatePrompt(
externalPR,
mockGitHubData,
false,
- mockTagMode,
+ "tag",
);
// Should have same tool access patterns