refactor: simplify mode system by removing Mode interface and registry (#899)

Replace the over-engineered Mode interface/registry/detector pattern with
straightforward inline logic. There are only 2 modes (tag and agent) and
the complexity wasn't justified.

- Delete Mode interface, registry, and prepare pass-through modules
- Export prepareTagMode() and prepareAgentMode() as standalone functions
- Inline trigger checking and mode dispatch in run.ts/prepare.ts
- Change generatePrompt/createPrompt to take modeName string instead of Mode
- Remove dead code (extractGitHubContext, unused detector helpers)
- Update CLAUDE.md to reflect new architecture
This commit is contained in:
Ashwin Bhat 2026-02-05 17:22:30 -08:00 committed by GitHub
parent f09dc9a6a3
commit 7057f3318b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 390 additions and 1300 deletions

View File

@ -11,7 +11,7 @@ bun run format:check # Check formatting
## What This Is ## 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 ## 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`. **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 ## Things That Will Bite You

View File

@ -20,7 +20,6 @@ import {
import type { ParsedGitHubContext } from "../github/context"; import type { ParsedGitHubContext } from "../github/context";
import type { CommonFields, PreparedContext, EventData } from "./types"; import type { CommonFields, PreparedContext, EventData } from "./types";
import { GITHUB_SERVER_URL } from "../github/api/config"; import { GITHUB_SERVER_URL } from "../github/api/config";
import type { Mode, ModeContext } from "../modes/types";
import { extractUserRequest } from "../utils/extract-user-request"; import { extractUserRequest } from "../utils/extract-user-request";
export type { CommonFields, PreparedContext } from "./types"; export type { CommonFields, PreparedContext } from "./types";
@ -458,9 +457,31 @@ export function generatePrompt(
context: PreparedContext, context: PreparedContext,
githubData: FetchDataResult, githubData: FetchDataResult,
useCommitSigning: boolean, useCommitSigning: boolean,
mode: Mode, modeName: "tag" | "agent",
): string { ): 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;
} }
/** /**
@ -901,28 +922,20 @@ function extractUserRequestFromContext(
} }
export async function createPrompt( export async function createPrompt(
mode: Mode, commentId: number,
modeContext: ModeContext, baseBranch: string | undefined,
claudeBranch: string | undefined,
githubData: FetchDataResult, githubData: FetchDataResult,
context: ParsedGitHubContext, context: ParsedGitHubContext,
) { ) {
try { try {
// Prepare the context for prompt generation const claudeCommentId = commentId.toString();
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 preparedContext = prepareContext( const preparedContext = prepareContext(
context, context,
claudeCommentId, claudeCommentId,
modeContext.baseBranch, baseBranch,
modeContext.claudeBranch, claudeBranch,
); );
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, { await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
@ -934,7 +947,7 @@ export async function createPrompt(
preparedContext, preparedContext,
githubData, githubData,
context.inputs.useCommitSigning, context.inputs.useCommitSigning,
mode, "tag",
); );
// Log the final prompt to console // Log the final prompt to console
@ -967,19 +980,12 @@ export async function createPrompt(
// Set allowed tools // Set allowed tools
const hasActionsReadPermission = false; const hasActionsReadPermission = false;
// Get mode-specific tools
const modeAllowedTools = mode.getAllowedTools();
const modeDisallowedTools = mode.getDisallowedTools();
const allAllowedTools = buildAllowedToolsString( const allAllowedTools = buildAllowedToolsString(
modeAllowedTools, [],
hasActionsReadPermission, hasActionsReadPermission,
context.inputs.useCommitSigning, context.inputs.useCommitSigning,
); );
const allDisallowedTools = buildDisallowedToolsString( const allDisallowedTools = buildDisallowedToolsString([], []);
modeDisallowedTools,
modeAllowedTools,
);
core.exportVariable("ALLOWED_TOOLS", allAllowedTools); core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools); core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);

View File

@ -10,8 +10,10 @@ import { setupGitHubToken } from "../github/token";
import { checkWritePermissions } from "../github/validation/permissions"; import { checkWritePermissions } from "../github/validation/permissions";
import { createOctokit } from "../github/api/client"; import { createOctokit } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context"; import { parseGitHubContext, isEntityContext } from "../github/context";
import { getMode } from "../modes/registry"; import { detectMode } from "../modes/detector";
import { prepare } from "../prepare"; import { prepareTagMode } from "../modes/tag";
import { prepareAgentMode } from "../modes/agent";
import { checkContainsTrigger } from "../github/validation/trigger";
import { collectActionInputsPresence } from "./collect-inputs"; import { collectActionInputsPresence } from "./collect-inputs";
async function run() { async function run() {
@ -22,7 +24,10 @@ async function run() {
const context = parseGitHubContext(); const context = parseGitHubContext();
// Auto-detect mode based on context // 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 // Setup GitHub token
const githubToken = await setupGitHubToken(); const githubToken = await setupGitHubToken();
@ -46,10 +51,13 @@ async function run() {
} }
// Check trigger conditions // Check trigger conditions
const containsTrigger = mode.shouldTrigger(context); const containsTrigger =
modeName === "tag"
? isEntityContext(context) && checkContainsTrigger(context)
: !!context.inputs?.prompt;
// Debug logging // Debug logging
console.log(`Mode: ${mode.name}`); console.log(`Mode: ${modeName}`);
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`); console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
console.log(`Trigger result: ${containsTrigger}`); console.log(`Trigger result: ${containsTrigger}`);
@ -63,31 +71,20 @@ async function run() {
return; return;
} }
// Step 5: Use the new modular prepare function // Run prepare
const result = await prepare({ console.log(
context, `Preparing with mode: ${modeName} for event: ${context.eventName}`,
octokit, );
mode, if (modeName === "tag") {
githubToken, 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 // 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 // Expose the GitHub token (Claude App token) as an output
core.setOutput("github_token", githubToken); 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) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
core.setFailed(`Prepare step failed with error: ${errorMessage}`); core.setFailed(`Prepare step failed with error: ${errorMessage}`);

View File

@ -17,8 +17,10 @@ import { createOctokit } from "../github/api/client";
import type { Octokits } from "../github/api/client"; import type { Octokits } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context"; import { parseGitHubContext, isEntityContext } from "../github/context";
import type { GitHubContext } from "../github/context"; import type { GitHubContext } from "../github/context";
import { getMode } from "../modes/registry"; import { detectMode } from "../modes/detector";
import { prepare } from "../prepare"; import { prepareTagMode } from "../modes/tag";
import { prepareAgentMode } from "../modes/agent";
import { checkContainsTrigger } from "../github/validation/trigger";
import { collectActionInputsPresence } from "./collect-inputs"; import { collectActionInputsPresence } from "./collect-inputs";
import { updateCommentLink } from "./update-comment-link"; import { updateCommentLink } from "./update-comment-link";
import { formatTurnsFromData } from "./format-turns"; import { formatTurnsFromData } from "./format-turns";
@ -138,7 +140,10 @@ async function run() {
// Phase 1: Prepare // Phase 1: Prepare
const actionInputsPresent = collectActionInputsPresence(); const actionInputsPresent = collectActionInputsPresence();
context = parseGitHubContext(); context = parseGitHubContext();
const mode = getMode(context); const modeName = detectMode(context);
console.log(
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
);
try { try {
githubToken = await setupGitHubToken(); githubToken = await setupGitHubToken();
@ -173,8 +178,11 @@ async function run() {
} }
// Check trigger conditions // Check trigger conditions
const containsTrigger = mode.shouldTrigger(context); const containsTrigger =
console.log(`Mode: ${mode.name}`); modeName === "tag"
? isEntityContext(context) && checkContainsTrigger(context)
: !!context.inputs?.prompt;
console.log(`Mode: ${modeName}`);
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`); console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
console.log(`Trigger result: ${containsTrigger}`); console.log(`Trigger result: ${containsTrigger}`);
@ -185,31 +193,19 @@ async function run() {
} }
// Run prepare // Run prepare
const prepareResult = await prepare({ console.log(
context, `Preparing with mode: ${modeName} for event: ${context.eventName}`,
octokit, );
mode, const prepareResult =
githubToken, modeName === "tag"
}); ? await prepareTagMode({ context, octokit, githubToken })
: await prepareAgentMode({ context, octokit, githubToken });
commentId = prepareResult.commentId; commentId = prepareResult.commentId;
claudeBranch = prepareResult.branchInfo.claudeBranch; claudeBranch = prepareResult.branchInfo.claudeBranch;
baseBranch = prepareResult.branchInfo.baseBranch; baseBranch = prepareResult.branchInfo.baseBranch;
prepareCompleted = true; 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 // Phase 2: Install Claude Code CLI
await installClaudeCode(); await installClaudeCode();

View File

@ -1,7 +1,4 @@
import * as core from "@actions/core";
import { mkdir, writeFile } from "fs/promises"; 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 { prepareMcpConfig } from "../../mcp/install-mcp-server";
import { parseAllowedTools } from "./parse-tools"; import { parseAllowedTools } from "./parse-tools";
import { import {
@ -10,90 +7,31 @@ import {
} from "../../github/operations/git-config"; } from "../../github/operations/git-config";
import { checkHumanActor } from "../../github/validation/actor"; import { checkHumanActor } from "../../github/validation/actor";
import type { GitHubContext } from "../../github/context"; 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 * Prepares the agent mode execution context.
*/
function extractGitHubContext(context: GitHubContext): Record<string, string> {
const envVars: Record<string, string> = {};
// 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.
* *
* 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, * It bypasses the standard @claude mention checking and comment tracking used by tag mode,
* providing direct access to Claude Code for automation workflows. * providing direct access to Claude Code for automation workflows.
*/ */
export const agentMode: Mode = { export async function prepareAgentMode({
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,
};
},
getAllowedTools() {
return [];
},
getDisallowedTools() {
return [];
},
shouldCreateTrackingComment() {
return false;
},
async prepare({
context, context,
octokit, octokit,
githubToken, githubToken,
}: ModeOptions): Promise<ModeResult> { }: {
context: GitHubContext;
octokit: Octokits;
githubToken: string;
}) {
// Check if actor is human (prevents bot-triggered loops) // Check if actor is human (prevents bot-triggered loops)
await checkHumanActor(octokit.rest, context); await checkHumanActor(octokit.rest, context);
// Configure git authentication for agent mode (same as tag mode) // Configure git authentication for agent mode (same as tag mode)
// SSH signing takes precedence if provided // SSH signing takes precedence if provided
const useSshSigning = !!context.inputs.sshSigningKey; const useSshSigning = !!context.inputs.sshSigningKey;
const useApiCommitSigning = const useApiCommitSigning = context.inputs.useCommitSigning && !useSshSigning;
context.inputs.useCommitSigning && !useSshSigning;
if (useSshSigning) { if (useSshSigning) {
// Setup SSH signing for commits // Setup SSH signing for commits
@ -193,28 +131,4 @@ export const agentMode: Mode = {
mcpConfig: ourMcpConfig, mcpConfig: ourMcpConfig,
claudeArgs, 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);
}
}
// Agent mode uses prompt field
if (context.prompt) {
return context.prompt;
}
// Minimal fallback - repository is a string in PreparedContext
return `Repository: ${context.repository}`;
},
getSystemPrompt() {
// Agent mode doesn't need additional system prompts
return undefined;
},
};

View File

@ -80,17 +80,6 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
return "agent"; 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 { function validateTrackProgressEvent(context: GitHubContext): void {
// track_progress is only valid for pull_request and issue events // track_progress is only valid for pull_request and issue events
const validEvents = [ 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;
}
}

View File

@ -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<AutoDetectedMode, Mode>;
/**
* 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);
}

View File

@ -1,5 +1,3 @@
import type { Mode, ModeOptions, ModeResult } from "../types";
import { checkContainsTrigger } from "../../github/validation/trigger";
import { checkHumanActor } from "../../github/validation/actor"; import { checkHumanActor } from "../../github/validation/actor";
import { createInitialComment } from "../../github/operations/comments/create-initial"; import { createInitialComment } from "../../github/operations/comments/create-initial";
import { setupBranch } from "../../github/operations/branch"; import { setupBranch } from "../../github/operations/branch";
@ -14,58 +12,27 @@ import {
extractOriginalTitle, extractOriginalTitle,
extractOriginalBody, extractOriginalBody,
} from "../../github/data/fetcher"; } from "../../github/data/fetcher";
import { createPrompt, generateDefaultPrompt } from "../../create-prompt"; import { createPrompt } from "../../create-prompt";
import { isEntityContext } from "../../github/context"; import { isEntityContext } from "../../github/context";
import type { PreparedContext } from "../../create-prompt/types"; import type { GitHubContext } from "../../github/context";
import type { FetchDataResult } from "../../github/data/fetcher"; import type { Octokits } from "../../github/api/client";
import { parseAllowedTools } from "../agent/parse-tools"; import { parseAllowedTools } from "../agent/parse-tools";
/** /**
* Tag mode implementation. * Prepares the tag mode execution context.
* *
* The traditional implementation mode that responds to @claude mentions, * Tag mode responds to @claude mentions, issue assignments, or labels.
* issue assignments, or labels. Creates tracking comments showing progress * Creates tracking comments showing progress and has full implementation capabilities.
* and has full implementation capabilities.
*/ */
export const tagMode: Mode = { export async function prepareTagMode({
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,
};
},
getAllowedTools() {
return [];
},
getDisallowedTools() {
return [];
},
shouldCreateTrackingComment() {
return true;
},
async prepare({
context, context,
octokit, octokit,
githubToken, githubToken,
}: ModeOptions): Promise<ModeResult> { }: {
context: GitHubContext;
octokit: Octokits;
githubToken: string;
}) {
// Tag mode only handles entity-based events // Tag mode only handles entity-based events
if (!isEntityContext(context)) { if (!isEntityContext(context)) {
throw new Error("Tag mode requires entity context"); throw new Error("Tag mode requires entity context");
@ -101,8 +68,7 @@ export const tagMode: Mode = {
// Configure git authentication // Configure git authentication
// SSH signing takes precedence if provided // SSH signing takes precedence if provided
const useSshSigning = !!context.inputs.sshSigningKey; const useSshSigning = !!context.inputs.sshSigningKey;
const useApiCommitSigning = const useApiCommitSigning = context.inputs.useCommitSigning && !useSshSigning;
context.inputs.useCommitSigning && !useSshSigning;
if (useSshSigning) { if (useSshSigning) {
// Setup SSH signing for commits // Setup SSH signing for commits
@ -135,17 +101,17 @@ export const tagMode: Mode = {
} }
// Create prompt file // Create prompt file
const modeContext = this.prepareContext(context, { await createPrompt(
commentId, commentId,
baseBranch: branchInfo.baseBranch, branchInfo.baseBranch,
claudeBranch: branchInfo.claudeBranch, branchInfo.claudeBranch,
}); githubData,
context,
await createPrompt(tagMode, modeContext, githubData, context); );
const userClaudeArgs = process.env.CLAUDE_ARGS || ""; const userClaudeArgs = process.env.CLAUDE_ARGS || "";
const userAllowedMCPTools = parseAllowedTools(userClaudeArgs).filter( const userAllowedMCPTools = parseAllowedTools(userClaudeArgs).filter((tool) =>
(tool) => tool.startsWith("mcp__github_"), tool.startsWith("mcp__github_"),
); );
// Build claude_args for tag mode with required tools // Build claude_args for tag mode with required tools
@ -219,36 +185,4 @@ export const tagMode: Mode = {
mcpConfig: ourMcpConfig, mcpConfig: ourMcpConfig,
claudeArgs: claudeArgs.trim(), claudeArgs: claudeArgs.trim(),
}; };
}, }
generatePrompt(
context: PreparedContext,
githubData: FetchDataResult,
useCommitSigning: boolean,
): string {
const defaultPrompt = generateDefaultPrompt(
context,
githubData,
useCommitSigning,
);
// If a custom prompt is provided, inject it into the tag mode prompt
if (context.githubContext?.inputs?.prompt) {
return (
defaultPrompt +
`
<custom_instructions>
${context.githubContext.inputs.prompt}
</custom_instructions>`
);
}
return defaultPrompt;
},
getSystemPrompt() {
// Tag mode doesn't need additional system prompts
return undefined;
},
};

View File

@ -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<ModeResult>;
/**
* 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;
};

View File

@ -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<PrepareResult> {
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,
});
}

View File

@ -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;
};

View File

@ -3,60 +3,13 @@
import { describe, test, expect } from "bun:test"; import { describe, test, expect } from "bun:test";
import { import {
generatePrompt, generatePrompt,
generateDefaultPrompt,
getEventTypeAndContext, getEventTypeAndContext,
buildAllowedToolsString, buildAllowedToolsString,
buildDisallowedToolsString, buildDisallowedToolsString,
} from "../src/create-prompt"; } from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt"; import type { PreparedContext } from "../src/create-prompt";
import type { Mode } from "../src/modes/types";
describe("generatePrompt", () => { 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 = { const mockGitHubData = {
contextData: { contextData: {
title: "Test PR", title: "Test PR",
@ -181,12 +134,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("You are Claude, an AI assistant"); expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>"); expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
@ -214,12 +162,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>"); expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>"); expect(prompt).toContain("<is_pr>true</is_pr>");
@ -245,12 +188,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>"); expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
expect(prompt).toContain( expect(prompt).toContain(
@ -278,12 +216,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>"); expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
expect(prompt).toContain( expect(prompt).toContain(
@ -310,12 +243,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>"); expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
expect(prompt).toContain( expect(prompt).toContain(
@ -341,12 +269,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>"); expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>"); expect(prompt).toContain("<is_pr>true</is_pr>");
@ -370,12 +293,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Verify prompt generates successfully without custom instructions // Verify prompt generates successfully without custom instructions
expect(prompt).toContain("@claude please fix this"); expect(prompt).toContain("@claude please fix this");
@ -400,7 +318,7 @@ describe("generatePrompt", () => {
envVars, envVars,
mockGitHubData, mockGitHubData,
false, false,
mockAgentMode, "agent",
); );
// Agent mode: Prompt is passed through as-is // Agent mode: Prompt is passed through as-is
@ -441,7 +359,7 @@ describe("generatePrompt", () => {
envVars, envVars,
mockGitHubData, mockGitHubData,
false, false,
mockAgentMode, "agent",
); );
// v1.0: Variables are NOT substituted - prompt is passed as-is to Claude Code // v1.0: Variables are NOT substituted - prompt is passed as-is to Claude Code
@ -490,7 +408,7 @@ describe("generatePrompt", () => {
envVars, envVars,
issueGitHubData, issueGitHubData,
false, false,
mockAgentMode, "agent",
); );
// Agent mode: Prompt is passed through as-is // Agent mode: Prompt is passed through as-is
@ -515,7 +433,7 @@ describe("generatePrompt", () => {
envVars, envVars,
mockGitHubData, mockGitHubData,
false, false,
mockAgentMode, "agent",
); );
// Agent mode: No substitution - passed as-is // Agent mode: No substitution - passed as-is
@ -539,12 +457,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("You are Claude, an AI assistant"); expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>"); expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
@ -567,12 +480,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>"); expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
// With commit signing disabled, co-author info appears in git commit instructions // With commit signing disabled, co-author info appears in git commit instructions
@ -594,12 +502,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain PR-specific instructions (git commands when not using signing) // Should contain PR-specific instructions (git commands when not using signing)
expect(prompt).toContain("git push"); expect(prompt).toContain("git push");
@ -630,12 +533,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain Issue-specific instructions // Should contain Issue-specific instructions
expect(prompt).toContain( expect(prompt).toContain(
@ -674,12 +572,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain the actual branch name with timestamp // Should contain the actual branch name with timestamp
expect(prompt).toContain( expect(prompt).toContain(
@ -709,12 +602,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain branch-specific instructions like issues // Should contain branch-specific instructions like issues
expect(prompt).toContain( expect(prompt).toContain(
@ -752,12 +640,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain open PR instructions (git commands when not using signing) // Should contain open PR instructions (git commands when not using signing)
expect(prompt).toContain("git push"); expect(prompt).toContain("git push");
@ -788,12 +671,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain new branch instructions // Should contain new branch instructions
expect(prompt).toContain( expect(prompt).toContain(
@ -821,12 +699,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain new branch instructions // Should contain new branch instructions
expect(prompt).toContain( expect(prompt).toContain(
@ -854,12 +727,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain new branch instructions // Should contain new branch instructions
expect(prompt).toContain( expect(prompt).toContain(
@ -883,12 +751,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should have git command instructions // Should have git command instructions
expect(prompt).toContain("Use git commands via the Bash tool"); expect(prompt).toContain("Use git commands via the Bash tool");
@ -917,12 +780,7 @@ describe("generatePrompt", () => {
}, },
}; };
const prompt = await generatePrompt( const prompt = await generatePrompt(envVars, mockGitHubData, true, "tag");
envVars,
mockGitHubData,
true,
mockTagMode,
);
// Should have commit signing tool instructions // Should have commit signing tool instructions
expect(prompt).toContain("mcp__github_file_ops__commit_files"); expect(prompt).toContain("mcp__github_file_ops__commit_files");

View File

@ -7,22 +7,17 @@ import {
spyOn, spyOn,
mock, mock,
} from "bun:test"; } from "bun:test";
import { agentMode } from "../../src/modes/agent"; import { prepareAgentMode } from "../../src/modes/agent";
import type { GitHubContext } from "../../src/github/context"; import { createMockAutomationContext } from "../mockContext";
import { createMockContext, createMockAutomationContext } from "../mockContext";
import * as core from "@actions/core"; import * as core from "@actions/core";
import * as gitConfig from "../../src/github/operations/git-config"; import * as gitConfig from "../../src/github/operations/git-config";
describe("Agent Mode", () => { describe("Agent Mode", () => {
let mockContext: GitHubContext;
let exportVariableSpy: any; let exportVariableSpy: any;
let setOutputSpy: any; let setOutputSpy: any;
let configureGitAuthSpy: any; let configureGitAuthSpy: any;
beforeEach(() => { beforeEach(() => {
mockContext = createMockAutomationContext({
eventName: "workflow_dispatch",
});
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation( exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
() => {}, () => {},
); );
@ -45,84 +40,11 @@ describe("Agent Mode", () => {
configureGitAuthSpy?.mockRestore(); configureGitAuthSpy?.mockRestore();
}); });
test("agent mode has correct properties", () => { test("prepareAgentMode is exported as a function", () => {
expect(agentMode.name).toBe("agent"); expect(typeof prepareAgentMode).toBe("function");
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", () => { test("prepare passes through claude_args", async () => {
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("prepare method passes through claude_args", async () => {
// Clear any previous calls before this test // Clear any previous calls before this test
exportVariableSpy.mockClear(); exportVariableSpy.mockClear();
setOutputSpy.mockClear(); setOutputSpy.mockClear();
@ -156,7 +78,7 @@ describe("Agent Mode", () => {
}, },
}, },
} as any; } as any;
const result = await agentMode.prepare({ const result = await prepareAgentMode({
context: contextWithCustomArgs, context: contextWithCustomArgs,
octokit: mockOctokit, octokit: mockOctokit,
githubToken: "test-token", githubToken: "test-token",
@ -186,7 +108,7 @@ describe("Agent Mode", () => {
process.env.GITHUB_REF_NAME = originalRefName; 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({ const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch", eventName: "workflow_dispatch",
}); });
@ -206,7 +128,7 @@ describe("Agent Mode", () => {
} as any; } as any;
await expect( await expect(
agentMode.prepare({ prepareAgentMode({
context: contextWithPrompts, context: contextWithPrompts,
octokit: mockOctokit, octokit: mockOctokit,
githubToken: "test-token", 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({ const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch", eventName: "workflow_dispatch",
}); });
@ -237,7 +159,7 @@ describe("Agent Mode", () => {
// Should not throw - bot is in allowed list // Should not throw - bot is in allowed list
await expect( await expect(
agentMode.prepare({ prepareAgentMode({
context: contextWithPrompts, context: contextWithPrompts,
octokit: mockOctokit, octokit: mockOctokit,
githubToken: "test-token", githubToken: "test-token",
@ -245,7 +167,7 @@ describe("Agent Mode", () => {
).resolves.toBeDefined(); ).resolves.toBeDefined();
}); });
test("prepare method creates prompt file with correct content", async () => { test("prepare creates prompt file with correct content", async () => {
const contextWithPrompts = createMockAutomationContext({ const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch", eventName: "workflow_dispatch",
}); });
@ -268,7 +190,7 @@ describe("Agent Mode", () => {
}, },
}, },
} as any; } as any;
const result = await agentMode.prepare({ const result = await prepareAgentMode({
context: contextWithPrompts, context: contextWithPrompts,
octokit: mockOctokit, octokit: mockOctokit,
githubToken: "test-token", githubToken: "test-token",

View File

@ -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);
});
});

View File

@ -1,92 +1,8 @@
import { describe, test, expect, beforeEach } from "bun:test"; import { describe, test, expect } from "bun:test";
import { tagMode } from "../../src/modes/tag"; import { prepareTagMode } from "../../src/modes/tag";
import type { ParsedGitHubContext } from "../../src/github/context";
import type { IssueCommentEvent } from "@octokit/webhooks-types";
import { createMockContext } from "../mockContext";
describe("Tag Mode", () => { describe("Tag Mode", () => {
let mockContext: ParsedGitHubContext; test("prepareTagMode is exported as a function", () => {
expect(typeof prepareTagMode).toBe("function");
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([]);
}); });
}); });

View File

@ -1,38 +1,10 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { describe, test, expect } from "bun:test"; import { describe, test, expect } from "bun:test";
import { import { getEventTypeAndContext, generatePrompt } from "../src/create-prompt";
getEventTypeAndContext,
generatePrompt,
generateDefaultPrompt,
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt"; import type { PreparedContext } from "../src/create-prompt";
import type { Mode } from "../src/modes/types";
describe("pull_request_target event support", () => { 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 = { const mockGitHubData = {
contextData: { contextData: {
title: "External PR via pull_request_target", title: "External PR via pull_request_target",
@ -126,12 +98,7 @@ describe("pull_request_target event support", () => {
}, },
}; };
const prompt = generatePrompt( const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain pull request event type and metadata // Should contain pull request event type and metadata
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>"); expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
@ -165,12 +132,7 @@ describe("pull_request_target event support", () => {
}, },
}; };
const prompt = generatePrompt( const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should include git commands for non-commit-signing mode // Should include git commands for non-commit-signing mode
expect(prompt).toContain("git push"); 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 // Should include commit signing tools
expect(prompt).toContain("mcp__github_file_ops__commit_files"); expect(prompt).toContain("mcp__github_file_ops__commit_files");
@ -246,13 +208,13 @@ describe("pull_request_target event support", () => {
pullRequestContext, pullRequestContext,
mockGitHubData, mockGitHubData,
false, false,
mockTagMode, "tag",
); );
const pullRequestTargetPrompt = generatePrompt( const pullRequestTargetPrompt = generatePrompt(
pullRequestTargetContext, pullRequestTargetContext,
mockGitHubData, mockGitHubData,
false, false,
mockTagMode, "tag",
); );
// Both should have the same event type and structure // 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 prompt = generatePrompt(envVars, mockGitHubData, false, "agent");
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,
);
expect(prompt).toBe( expect(prompt).toBe(
"Review this pull_request_target PR for security issues", "Review this pull_request_target PR for security issues",
@ -343,12 +275,7 @@ describe("pull_request_target event support", () => {
}, },
}; };
const prompt = generatePrompt( const prompt = generatePrompt(envVars, mockGitHubData, false, "tag");
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should generate default prompt structure // Should generate default prompt structure
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>"); expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
@ -418,7 +345,7 @@ describe("pull_request_target event support", () => {
// Should not throw when generating prompt // Should not throw when generating prompt
expect(() => { expect(() => {
generatePrompt(minimalContext, mockGitHubData, false, mockTagMode); generatePrompt(minimalContext, mockGitHubData, false, "tag");
}).not.toThrow(); }).not.toThrow();
}); });
@ -476,13 +403,13 @@ describe("pull_request_target event support", () => {
internalPR, internalPR,
mockGitHubData, mockGitHubData,
false, false,
mockTagMode, "tag",
); );
const externalPrompt = generatePrompt( const externalPrompt = generatePrompt(
externalPR, externalPR,
mockGitHubData, mockGitHubData,
false, false,
mockTagMode, "tag",
); );
// Should have same tool access patterns // Should have same tool access patterns