diff --git a/action.yml b/action.yml index c36db83..6b32ede 100644 --- a/action.yml +++ b/action.yml @@ -28,7 +28,7 @@ inputs: required: false default: "" allowed_bots: - description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots." + description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots. WARNING: On public repos with '*', external Apps may be able to invoke this action with prompts they control. See docs/security.md." required: false default: "" allowed_non_write_users: @@ -97,6 +97,10 @@ inputs: description: "Use just one comment to deliver issue/PR comments" required: false default: "false" + classify_inline_comments: + description: "Buffer inline comments without confirmed=true and classify them (real review vs test/probe) before posting after the session ends. Set to 'false' to post all inline comments immediately (pre-buffering behavior)." + required: false + default: "true" use_commit_signing: description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands" required: false @@ -252,6 +256,7 @@ runs: EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }} GITHUB_RUN_ID: ${{ github.run_id }} USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }} + CLASSIFY_INLINE_COMMENTS: ${{ inputs.classify_inline_comments }} DEFAULT_WORKFLOW_TOKEN: ${{ github.token }} USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }} SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }} @@ -334,6 +339,18 @@ runs: run: | bun run ${GITHUB_ACTION_PATH}/src/entrypoints/cleanup-ssh-signing.ts + - name: Post buffered inline comments + if: always() && inputs.classify_inline_comments != 'false' + shell: bash + env: + GITHUB_TOKEN: ${{ steps.run.outputs.github_token || inputs.github_token || github.token }} + REPO_OWNER: ${{ github.event.repository.owner.login }} + REPO_NAME: ${{ github.event.repository.name }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + run: | + bun run ${GITHUB_ACTION_PATH}/src/entrypoints/post-buffered-inline-comments.ts + - name: Revoke app token if: always() && inputs.github_token == '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true' && steps.detect-platform.outputs.platform != 'forgejo' shell: bash diff --git a/base-action/action.yml b/base-action/action.yml index 038be98..31d7928 100644 --- a/base-action/action.yml +++ b/base-action/action.yml @@ -124,7 +124,7 @@ runs: PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} run: | if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then - CLAUDE_CODE_VERSION="2.1.71" + CLAUDE_CODE_VERSION="2.1.76" echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..." for attempt in 1 2 3; do echo "Installation attempt $attempt..." diff --git a/base-action/bun.lock b/base-action/bun.lock index b00a73c..8be9f43 100644 --- a/base-action/bun.lock +++ b/base-action/bun.lock @@ -6,7 +6,7 @@ "name": "@anthropic-ai/claude-code-base-action", "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.71", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "shell-quote": "^1.8.3", }, "devDependencies": { @@ -27,7 +27,7 @@ "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.71", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pIsQJnM7Y+cJHL7aFY6SCCW3FIni218gVEpPqG8XGowfYxboFNBbNssWiUNRwthT8bp9jypcX7q5kx0Xsw14xg=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.76", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/base-action/package.json b/base-action/package.json index d20f324..2c72e73 100644 --- a/base-action/package.json +++ b/base-action/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@actions/core": "^1.10.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.71", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "shell-quote": "^1.8.3" }, "devDependencies": { diff --git a/bun.lock b/bun.lock index 715b0f6..88e171c 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.71", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", @@ -37,7 +37,7 @@ "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.71", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-pIsQJnM7Y+cJHL7aFY6SCCW3FIni218gVEpPqG8XGowfYxboFNBbNssWiUNRwthT8bp9jypcX7q5kx0Xsw14xg=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.76", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], diff --git a/docs/security.md b/docs/security.md index d36f061..eb3c69a 100644 --- a/docs/security.md +++ b/docs/security.md @@ -4,6 +4,10 @@ - **Repository Access**: The action can only be triggered by users with write access to the repository - **Bot User Control**: By default, GitHub Apps and bots cannot trigger this action for security reasons. Use the `allowed_bots` parameter to enable specific bots or all bots + - **⚠️ Allowed bots are not checked for repository permissions.** A bot that matches an entry does **not** need to be installed on your repository or have write access. On a **public repository**, external parties — including GitHub Apps created by anyone — may be able to trigger workflow events such as opening issues, commenting, or reviewing pull requests. If your workflow listens on those events and `allowed_bots` is set to `'*'`, any such App can invoke this action with a prompt it controls. + - Prefer an explicit list over `'*'` + - Only list App names you trust + - If you need `'*'`, scope workflow `permissions:` to the minimum required - **⚠️ Non-Write User Access (RISKY)**: The `allowed_non_write_users` parameter allows bypassing the write permission requirement. **This is a significant security risk and should only be used for workflows with extremely limited permissions** (e.g., issue labeling workflows that only have `issues: write` permission). This feature: - Only works when `github_token` is provided as input (not with GitHub App authentication) - Accepts either a comma-separated list of specific usernames or `*` to allow all users diff --git a/docs/solutions.md b/docs/solutions.md index 98f5d72..dd7fcb5 100644 --- a/docs/solutions.md +++ b/docs/solutions.md @@ -55,7 +55,7 @@ jobs: Note: The PR branch is already checked out in the current working directory. Use `gh pr comment` for top-level feedback. - Use `mcp__github_inline_comment__create_inline_comment` to highlight specific code issues. + Use `mcp__github_inline_comment__create_inline_comment` (with `confirmed: true`) to highlight specific code issues. Only post GitHub comments - don't submit review text as messages. claude_args: | @@ -585,7 +585,7 @@ prompt: | ### Common Tool Permissions - **PR Comments**: `Bash(gh pr comment:*)` -- **Inline Comments**: `mcp__github_inline_comment__create_inline_comment` +- **Inline Comments**: `mcp__github_inline_comment__create_inline_comment` — pass `confirmed: true` to post immediately. When omitted, the comment is buffered and classified after the session ends (real review comments post, test/probe comments are filtered). This prevents subagent test comments from reaching PRs. To disable classification entirely, set `classify_inline_comments: 'false'` on the action. - **File Operations**: `Read,Write,Edit` - **Git Operations**: `Bash(git:*)` diff --git a/docs/usage.md b/docs/usage.md index cc8ec24..6113839 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -52,36 +52,37 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | -| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | -| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - | -| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | -| `include_fix_links` | Include 'Fix this' links in PR code review feedback that open Claude Code with context to fix the identified issue | No | `true` | -| `claude_args` | Additional [arguments to pass directly to Claude CLI](https://docs.claude.com/en/docs/claude-code/cli-reference#cli-flags) (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" | -| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | -| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | -| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | -| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | -| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | -| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | -| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | -| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | -| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | -| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | -| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | -| `use_commit_signing` | Enable commit signing using GitHub's API. Simple but cannot perform complex git operations like rebasing. See [Security](./security.md#commit-signing) | No | `false` | -| `ssh_signing_key` | SSH private key for signing commits. Enables signed commits with full git CLI support (rebasing, etc.). See [Security](./security.md#commit-signing) | No | "" | -| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID). Required with `ssh_signing_key` for verified commits | No | `41898282` | -| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name). Required with `ssh_signing_key` for verified commits | No | `claude[bot]` | -| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" | -| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" | -| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" | -| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" | -| `plugin_marketplaces` | Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., see example in workflow above). Marketplaces are added before plugin installation | No | "" | -| `plugins` | Newline-separated list of Claude Code plugin names to install (e.g., see example in workflow above). Plugins are installed before Claude Code execution | No | "" | -| `platform` | Platform to run on: `auto` (default, auto-detects), `github`, or `forgejo`. See [Forgejo Setup](./forgejo.md) | No | `auto` | +| Input | Description | Required | Default | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | +| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | +| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | +| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - | +| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | +| `include_fix_links` | Include 'Fix this' links in PR code review feedback that open Claude Code with context to fix the identified issue | No | `true` | +| `claude_args` | Additional [arguments to pass directly to Claude CLI](https://docs.claude.com/en/docs/claude-code/cli-reference#cli-flags) (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" | +| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | +| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | +| `classify_inline_comments` | Buffer inline comments without `confirmed: true` and classify them (real review vs test/probe) via Haiku before posting after the session ends. Prevents subagent test comments. Set `'false'` to post all inline comments immediately | No | `true` | +| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | +| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | +| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | +| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | +| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | +| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | +| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | +| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | +| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | +| `use_commit_signing` | Enable commit signing using GitHub's API. Simple but cannot perform complex git operations like rebasing. See [Security](./security.md#commit-signing) | No | `false` | +| `ssh_signing_key` | SSH private key for signing commits. Enables signed commits with full git CLI support (rebasing, etc.). See [Security](./security.md#commit-signing) | No | "" | +| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID). Required with `ssh_signing_key` for verified commits | No | `41898282` | +| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name). Required with `ssh_signing_key` for verified commits | No | `claude[bot]` | +| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots. **⚠️ On public repos with `'*'`, external Apps may be able to invoke this action.** See [Security](./security.md) | No | "" | +| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" | +| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" | +| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" | +| `plugin_marketplaces` | Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., see example in workflow above). Marketplaces are added before plugin installation | No | "" | +| `plugins` | Newline-separated list of Claude Code plugin names to install (e.g., see example in workflow above). Plugins are installed before Claude Code execution | No | "" | +| `platform` | Platform to run on: `auto` (default, auto-detects), `github`, or `forgejo`. See [Forgejo Setup](./forgejo.md) | No | `auto` | ### Deprecated Inputs diff --git a/package.json b/package.json index e3f29c9..bbd8863 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.1", - "@anthropic-ai/claude-agent-sdk": "^0.2.71", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@modelcontextprotocol/sdk": "^1.11.0", "@octokit/graphql": "^8.2.2", "@octokit/rest": "^21.1.1", diff --git a/scripts/git-push.sh b/scripts/git-push.sh new file mode 100755 index 0000000..a8afcb1 --- /dev/null +++ b/scripts/git-push.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Wrapper around `git push` that only allows `origin ` with no flags. +# Defends against --receive-pack / --exec RCE and arbitrary-remote exfiltration +# (H1 #3556799). `git push:*` in allowedTools permits `git push --receive-pack='sh -c ...' ext::sh` +# which runs arbitrary shell on the Actions runner. This wrapper closes that. +# +# Usage: +# git-push.sh origin HEAD +# git-push.sh origin claude/issue-123-20260304 + +if [[ $# -ne 2 ]]; then + echo "Error: exactly two arguments required: origin " >&2 + exit 1 +fi + +for arg in "$@"; do + if [[ "$arg" == -* ]]; then + echo "Error: flags are not allowed (got: $arg)" >&2 + exit 1 + fi +done + +if [[ "$1" != "origin" ]]; then + echo "Error: remote must be 'origin' (got: $1)" >&2 + exit 1 +fi + +REF="$2" +if [[ "$REF" != "HEAD" ]] && ! git check-ref-format --branch "$REF" >/dev/null 2>&1; then + echo "Error: invalid ref: $REF" >&2 + exit 1 +fi + +exec git push origin "$REF" diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 7c8f41d..f8fa17e 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -26,19 +26,15 @@ import { extractUserRequest } from "../utils/extract-user-request"; import { parseToolsInput } from "../utils/parse-tools-input"; export type { CommonFields, PreparedContext } from "./types"; +const GIT_PUSH_WRAPPER = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`; + /** Filename for the user request file, read by the SDK runner */ const USER_REQUEST_FILENAME = "claude-user-request.txt"; -// Tag mode defaults - these tools are needed for tag mode to function -const BASE_ALLOWED_TOOLS = [ - "Edit", - "MultiEdit", - "Glob", - "Grep", - "LS", - "Read", - "Write", -]; +// Tag mode defaults - these tools are needed for tag mode to function. +// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode +// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside it. +const BASE_ALLOWED_TOOLS = ["Glob", "Grep", "LS", "Read"]; export function buildAllowedToolsString( customAllowedTools?: string[], @@ -62,7 +58,7 @@ export function buildAllowedToolsString( baseTools.push( "Bash(git add:*)", "Bash(git commit:*)", - "Bash(git push:*)", + `Bash(${GIT_PUSH_WRAPPER}:*)`, "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", @@ -443,7 +439,7 @@ function getCommitInstructions( Bash(git commit -m "\\n\\n${coAuthorLine}")` : "" } - - Push to the remote: Bash(git push origin HEAD)`; + - Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin HEAD)`; } else { const branchName = eventData.claudeBranch || eventData.baseBranch; return ` @@ -457,7 +453,7 @@ function getCommitInstructions( Bash(git commit -m "\\n\\n${coAuthorLine}")` : "" } - - Push to the remote: Bash(git push origin ${branchName})`; + - Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin ${branchName})`; } } } @@ -840,7 +836,7 @@ ${ : `- Use git commands via the Bash tool for version control (remember that you have access to these git commands): - Stage files: Bash(git add ) - Commit changes: Bash(git commit -m "") - - Push to remote: Bash(git push origin ) (NEVER force push) + - Push to remote: Bash(${GIT_PUSH_WRAPPER} origin ) - Delete files: Bash(git rm ) followed by commit and push - Check status: Bash(git status) - View diff: Bash(git diff)${eventData.isPR && eventData.baseBranch ? `\n - IMPORTANT: For PR diffs, use: Bash(git diff origin/${eventData.baseBranch}...HEAD)` : ""}` diff --git a/src/entrypoints/collect-inputs.ts b/src/entrypoints/collect-inputs.ts index a463743..079565c 100644 --- a/src/entrypoints/collect-inputs.ts +++ b/src/entrypoints/collect-inputs.ts @@ -23,6 +23,7 @@ export function collectActionInputsPresence(): string { github_token: "", max_turns: "", use_sticky_comment: "false", + classify_inline_comments: "true", use_commit_signing: "false", ssh_signing_key: "", }; diff --git a/src/entrypoints/post-buffered-inline-comments.ts b/src/entrypoints/post-buffered-inline-comments.ts new file mode 100644 index 0000000..763dae7 --- /dev/null +++ b/src/entrypoints/post-buffered-inline-comments.ts @@ -0,0 +1,233 @@ +#!/usr/bin/env bun +/** + * Reads buffered inline-comment calls from /tmp/inline-comments-buffer.jsonl, + * classifies each as "real review" vs "test/probe" using Haiku, and posts + * only the real ones. Calls with confirmed=false are never posted. + * + * If the Anthropic API is unavailable (Bedrock/Vertex users without a direct + * key), falls back to posting everything with confirmed !== false. This + * preserves backward compatibility — before this change, all unconfirmed + * calls posted immediately. + */ +import { readFileSync } from "fs"; +import { createOctokit } from "../github/api/client"; + +const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl"; + +type BufferedComment = { + ts: string; + path: string; + line?: number; + startLine?: number; + side?: "LEFT" | "RIGHT"; + commit_id?: string; + body: string; + confirmed?: boolean; +}; + +const CLASSIFICATION_PROMPT = `You are classifying PR inline comments as either REAL code review feedback or TEST/PROBE calls. + +A TEST/PROBE call is when an automated agent is checking whether a commenting tool works. These typically: +- Start with phrases like "Test comment", "Testing if", "Can I", "Does this work", "Checking if" +- Have generic/placeholder content not specific to any code +- Exist to verify tool functionality, not to provide review feedback + +A REAL review comment: +- Discusses specific code, logic, bugs, or style +- Provides actionable feedback for the PR author +- References concrete aspects of the change + +For each numbered comment body below, respond with ONLY a JSON array of booleans where true = REAL review comment, false = test/probe. No other text. + +Comments: +`; + +async function classifyComments(bodies: string[]): Promise { + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + console.log( + "ANTHROPIC_API_KEY not set — skipping classification, posting all unconfirmed comments", + ); + return null; + } + + const prompt = + CLASSIFICATION_PROMPT + + bodies.map((b, i) => `${i + 1}. ${JSON.stringify(b)}`).join("\n"); + + try { + const res = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "content-type": "application/json", + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify({ + model: "claude-haiku-4-5", + max_tokens: 1024, + messages: [{ role: "user", content: prompt }], + }), + }); + + if (!res.ok) { + console.log( + `Classification API returned ${res.status} — posting all unconfirmed comments`, + ); + return null; + } + + const data = (await res.json()) as { + content: { type: string; text: string }[]; + }; + const text = data.content.find((c) => c.type === "text")?.text ?? ""; + const match = text.match(/\[[\s\S]*\]/); + if (!match) { + console.log( + "Could not parse classification response — posting all unconfirmed comments", + ); + return null; + } + const parsed = JSON.parse(match[0]); + if ( + !Array.isArray(parsed) || + parsed.length !== bodies.length || + !parsed.every((v) => typeof v === "boolean") + ) { + console.log( + "Classification response shape mismatch — posting all unconfirmed comments", + ); + return null; + } + return parsed; + } catch (e) { + console.log( + `Classification failed (${e instanceof Error ? e.message : String(e)}) — posting all unconfirmed comments`, + ); + return null; + } +} + +async function postComment( + octokit: ReturnType["rest"], + owner: string, + repo: string, + pull_number: number, + headSha: string, + c: BufferedComment, +): Promise { + const params: Parameters[0] = { + owner, + repo, + pull_number, + body: c.body, + path: c.path, + side: c.side || "RIGHT", + commit_id: c.commit_id || headSha, + }; + if (c.startLine) { + params.start_line = c.startLine; + params.start_side = c.side || "RIGHT"; + params.line = c.line; + } else { + params.line = c.line; + } + try { + await octokit.rest.pulls.createReviewComment(params); + return true; + } catch (e) { + console.log( + ` failed ${c.path}:${c.line}: ${e instanceof Error ? e.message : String(e)}`, + ); + return false; + } +} + +async function main() { + let raw: string; + try { + raw = readFileSync(BUFFER_PATH, "utf8"); + } catch { + console.log("No buffered inline comments"); + return; + } + + const comments: BufferedComment[] = raw + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line)); + + if (comments.length === 0) { + console.log("No buffered inline comments"); + return; + } + + console.log(`Found ${comments.length} buffered inline comment(s)`); + + const githubToken = process.env.GITHUB_TOKEN; + const owner = process.env.REPO_OWNER; + const repo = process.env.REPO_NAME; + const prNumber = process.env.PR_NUMBER; + + if (!githubToken || !owner || !repo || !prNumber) { + console.log( + "::warning::Missing GITHUB_TOKEN/REPO_OWNER/REPO_NAME/PR_NUMBER — cannot post buffered comments", + ); + return; + } + + // Partition: confirmed=false are never posted; the rest are candidates + const neverPost = comments.filter((c) => c.confirmed === false); + const candidates = comments.filter((c) => c.confirmed !== false); + + if (neverPost.length > 0) { + console.log(` ${neverPost.length} with confirmed=false — not posting`); + } + + if (candidates.length === 0) { + return; + } + + // Classify candidates + const verdicts = await classifyComments(candidates.map((c) => c.body)); + const toPost = + verdicts === null + ? candidates + : candidates.filter((_, i) => verdicts[i] === true); + const filtered = + verdicts === null ? [] : candidates.filter((_, i) => verdicts[i] === false); + + if (filtered.length > 0) { + console.log( + `::warning::${filtered.length} buffered comment(s) classified as test/probe — NOT posted:`, + ); + for (const c of filtered) { + console.log(` [${c.path}:${c.line}] ${c.body.slice(0, 120)}`); + } + } + + if (toPost.length === 0) { + console.log("No real comments to post"); + return; + } + + const octokit = createOctokit(githubToken).rest; + const pull_number = parseInt(prNumber, 10); + const pr = await octokit.pulls.get({ owner, repo, pull_number }); + const headSha = pr.data.head.sha; + + console.log(`Posting ${toPost.length} classified-as-real comment(s)`); + let posted = 0; + for (const c of toPost) { + if (await postComment(octokit, owner, repo, pull_number, headSha, c)) { + console.log(` posted ${c.path}:${c.line}`); + posted++; + } + } + console.log(`Posted ${posted}/${toPost.length}`); +} + +main().catch((e) => { + console.error("post-buffered-inline-comments failed:", e); + process.exit(1); +}); diff --git a/src/entrypoints/run.ts b/src/entrypoints/run.ts index ff24840..95e16ba 100644 --- a/src/entrypoints/run.ts +++ b/src/entrypoints/run.ts @@ -54,7 +54,7 @@ async function installClaudeCode(): Promise { return; } - const claudeCodeVersion = "2.1.71"; + const claudeCodeVersion = "2.1.76"; console.log(`Installing Claude Code v${claudeCodeVersion}...`); // On musl/Alpine systems, the native binary may require a newer musl than diff --git a/src/github/context.ts b/src/github/context.ts index 714a73c..c16945e 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -115,6 +115,7 @@ type BaseContext = { branchPrefix: string; branchNameTemplate?: string; useStickyComment: boolean; + classifyInlineComments: boolean; useCommitSigning: boolean; sshSigningKey: string; botId: string; @@ -178,6 +179,7 @@ export function parseGitHubContext(): GitHubContext { branchPrefix: process.env.BRANCH_PREFIX ?? "claude/", branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE, useStickyComment: process.env.USE_STICKY_COMMENT === "true", + classifyInlineComments: process.env.CLASSIFY_INLINE_COMMENTS !== "false", useCommitSigning: process.env.USE_COMMIT_SIGNING === "true", sshSigningKey: process.env.SSH_SIGNING_KEY || "", botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), diff --git a/src/mcp/github-inline-comment-server.ts b/src/mcp/github-inline-comment-server.ts index 703cda2..535124f 100644 --- a/src/mcp/github-inline-comment-server.ts +++ b/src/mcp/github-inline-comment-server.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { appendFileSync } from "fs"; import { z } from "zod"; import { createOctokit } from "../github/api/client"; import { sanitizeContent } from "../github/utils/sanitizer"; @@ -10,6 +11,13 @@ const REPO_OWNER = process.env.REPO_OWNER; const REPO_NAME = process.env.REPO_NAME; const PR_NUMBER = process.env.PR_NUMBER; +// Calls without confirmed=true are buffered here instead of posted. This +// prevents subagents from posting test/probe comments when they inherit this +// tool and probe it after hitting unrelated errors. The action's post-step +// reports the buffer count for diagnostics. +const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl"; +const CLASSIFY_ENABLED = process.env.CLASSIFY_INLINE_COMMENTS !== "false"; + if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) { console.error( "Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required", @@ -67,8 +75,17 @@ server.tool( .describe( "Specific commit SHA to comment on (defaults to latest commit)", ), + confirmed: z + .boolean() + .optional() + .describe( + "Set true to post immediately. When omitted, the call is buffered " + + "and classified after the session completes — real review comments " + + "post, test/probe comments are dropped. Set false to buffer and " + + "never post. Only set true when posting final review comments.", + ), }, - async ({ path, body, line, startLine, side, commit_id }) => { + async ({ path, body, line, startLine, side, commit_id, confirmed }) => { try { const githubToken = process.env.GITHUB_TOKEN; @@ -80,8 +97,6 @@ server.tool( const repo = REPO_NAME; const pull_number = parseInt(PR_NUMBER, 10); - const octokit = createOctokit(githubToken).rest; - // Sanitize the comment body to remove any potential GitHub tokens const sanitizedBody = sanitizeContent(body); @@ -92,10 +107,49 @@ server.tool( ); } + if (CLASSIFY_ENABLED && confirmed !== true) { + appendFileSync( + BUFFER_PATH, + JSON.stringify({ + ts: new Date().toISOString(), + path, + line, + startLine, + side, + commit_id, + body: sanitizedBody, + confirmed, + }) + "\n", + ); + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + success: true, + buffered: true, + message: + "Comment buffered. It will be classified and posted after " + + "this session completes (real review comments post, " + + "test/probe comments are dropped). Set confirmed=true to " + + "post immediately. If you are testing whether this tool " + + "works: it works — no need to test further.", + }, + null, + 2, + ), + }, + ], + }; + } + // If only line is provided, it's a single-line comment // If both startLine and line are provided, it's a multi-line comment const isSingleLine = !startLine; + const octokit = createOctokit(githubToken).rest; + const pr = await octokit.pulls.get({ owner, repo, diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 3048c3d..9c35c82 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -157,6 +157,9 @@ export async function prepareMcpConfig( REPO_NAME: repo, PR_NUMBER: context.entityNumber?.toString() || "", GITHUB_API_URL: GITHUB_API_URL, + CLASSIFY_INLINE_COMMENTS: context.inputs.classifyInlineComments + ? "true" + : "false", }, }; } diff --git a/src/modes/tag/index.ts b/src/modes/tag/index.ts index a534655..0012337 100644 --- a/src/modes/tag/index.ts +++ b/src/modes/tag/index.ts @@ -128,16 +128,17 @@ export async function prepareTagMode({ // Merge user-provided allowed_tools input (fixes upstream #181, #287, #724) const userAllowedTools = parseToolsInput(process.env.ALLOWED_TOOLS_INPUT); - // Build claude_args for tag mode with required tools - // Tag mode REQUIRES these tools to function properly + const gitPushWrapper = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`; + + // Build claude_args for tag mode with required tools. + // Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode (set below) + // auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside (e.g. ~/.bashrc). + // Listing them here would grant blanket write access to the whole runner (Asana 1213310082312048). 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", @@ -152,7 +153,7 @@ export async function prepareTagMode({ tagModeTools.push( "Bash(git add:*)", "Bash(git commit:*)", - "Bash(git push:*)", + `Bash(${gitPushWrapper}:*)`, "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", @@ -186,8 +187,10 @@ export async function prepareTagMode({ const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''"); claudeArgs = `--mcp-config '${escapedOurConfig}'`; - // Add required tools for tag mode - claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`; + // Add required tools for tag mode. + // acceptEdits: file edits auto-allowed inside cwd ($GITHUB_WORKSPACE), denied outside. + // Headless SDK has no prompt handler, so anything that falls through to "ask" is denied. + claudeArgs += ` --permission-mode acceptEdits --allowedTools "${tagModeTools.join(",")}"`; // Append user's claude_args (which may have more --mcp-config flags) if (userClaudeArgs) { diff --git a/test/create-prompt.test.ts b/test/create-prompt.test.ts index 0a5ea82..3bf8b6c 100644 --- a/test/create-prompt.test.ts +++ b/test/create-prompt.test.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun -import { describe, test, expect } from "bun:test"; +import { describe, test, expect, beforeAll } from "bun:test"; import { generatePrompt, getEventTypeAndContext, @@ -9,6 +9,10 @@ import { } from "../src/create-prompt"; import type { PreparedContext } from "../src/create-prompt"; +beforeAll(() => { + process.env.GITHUB_ACTION_PATH = "/test/action/path"; +}); + describe("generatePrompt", () => { const mockGitHubData = { contextData: { @@ -505,7 +509,7 @@ describe("generatePrompt", () => { const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag"); // Should contain PR-specific instructions (git commands when not using signing) - expect(prompt).toContain("git push"); + expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain( "Always push to the existing branch when triggered on a PR", ); @@ -643,7 +647,7 @@ describe("generatePrompt", () => { const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag"); // Should contain open PR instructions (git commands when not using signing) - expect(prompt).toContain("git push"); + expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain( "Always push to the existing branch when triggered on a PR", ); @@ -757,7 +761,7 @@ describe("generatePrompt", () => { expect(prompt).toContain("Use git commands via the Bash tool"); expect(prompt).toContain("git add"); expect(prompt).toContain("git commit"); - expect(prompt).toContain("git push"); + expect(prompt).toContain("scripts/git-push.sh origin"); // Should use the minimal comment tool expect(prompt).toContain("mcp__github_comment__update_claude_comment"); @@ -886,17 +890,18 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString(); // The base tools should be in the result - expect(result).toContain("Edit"); + // Edit/MultiEdit/Write are NOT in allowedTools — acceptEdits permission mode handles them + expect(result).not.toContain("Edit"); + expect(result).not.toContain("Write"); expect(result).toContain("Glob"); expect(result).toContain("Grep"); expect(result).toContain("LS"); expect(result).toContain("Read"); - expect(result).toContain("Write"); // Default is no commit signing, so should have specific Bash git commands expect(result).toContain("Bash(git add:*)"); expect(result).toContain("Bash(git commit:*)"); - expect(result).toContain("Bash(git push:*)"); + expect(result).toContain("scripts/git-push.sh:*)"); expect(result).toContain("mcp__github_comment__update_claude_comment"); // Should not have commit signing tools @@ -908,12 +913,12 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString([], false, false); // The base tools should be in the result - expect(result).toContain("Edit"); + expect(result).not.toContain("Edit"); expect(result).toContain("Glob"); expect(result).toContain("Grep"); expect(result).toContain("LS"); expect(result).toContain("Read"); - expect(result).toContain("Write"); + expect(result).not.toContain("Write"); // Should have specific Bash git commands for non-signing mode expect(result).toContain("Bash(git add:*)"); @@ -930,7 +935,7 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString(customTools); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).toContain("Read"); expect(result).toContain("Glob"); // Custom tools should be appended @@ -950,7 +955,7 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString([], true); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).toContain("Read"); expect(result).toContain("Glob"); // GitHub Actions tools should be included @@ -964,7 +969,7 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString(customTools, true); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).toContain("Read"); // Custom tools should be included expect(result).toContain("Tool1"); @@ -980,12 +985,12 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString([], false, true); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).not.toContain("Edit"); expect(result).toContain("Glob"); expect(result).toContain("Grep"); expect(result).toContain("LS"); expect(result).toContain("Read"); - expect(result).toContain("Write"); + expect(result).not.toContain("Write"); // Commit signing tools should be included expect(result).toContain("mcp__github_file_ops__commit_files"); @@ -1001,17 +1006,17 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString([], false, false); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).not.toContain("Edit"); expect(result).toContain("Glob"); expect(result).toContain("Grep"); expect(result).toContain("LS"); expect(result).toContain("Read"); - expect(result).toContain("Write"); + expect(result).not.toContain("Write"); // Specific Bash git commands should be included expect(result).toContain("Bash(git add:*)"); expect(result).toContain("Bash(git commit:*)"); - expect(result).toContain("Bash(git push:*)"); + expect(result).toContain("scripts/git-push.sh:*)"); expect(result).toContain("Bash(git status:*)"); expect(result).toContain("Bash(git diff:*)"); expect(result).toContain("Bash(git log:*)"); @@ -1030,7 +1035,7 @@ describe("buildAllowedToolsString", () => { const result = buildAllowedToolsString(customTools, true, false); // Base tools should be present - expect(result).toContain("Edit"); + expect(result).toContain("Read"); expect(result).toContain("Bash(git add:*)"); // Custom tools should be included diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts index 9c79fe1..4f8f2fa 100644 --- a/test/install-mcp-server.test.ts +++ b/test/install-mcp-server.test.ts @@ -33,6 +33,7 @@ describe("prepareMcpConfig", () => { labelTrigger: "", branchPrefix: "", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), diff --git a/test/mockContext.ts b/test/mockContext.ts index 8855132..ae397cd 100644 --- a/test/mockContext.ts +++ b/test/mockContext.ts @@ -19,6 +19,7 @@ const defaultInputs = { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), diff --git a/test/modes/detector.test.ts b/test/modes/detector.test.ts index 436e646..3f29af6 100644 --- a/test/modes/detector.test.ts +++ b/test/modes/detector.test.ts @@ -19,6 +19,7 @@ describe("detectMode with enhanced routing", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, sshSigningKey: "", botId: "123456", diff --git a/test/permissions.test.ts b/test/permissions.test.ts index ac456da..ad49256 100644 --- a/test/permissions.test.ts +++ b/test/permissions.test.ts @@ -68,6 +68,7 @@ describe("checkWritePermissions", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, sshSigningKey: "", botId: String(CLAUDE_APP_BOT_ID), diff --git a/test/pull-request-target.test.ts b/test/pull-request-target.test.ts index 59c747f..6d9cac0 100644 --- a/test/pull-request-target.test.ts +++ b/test/pull-request-target.test.ts @@ -135,7 +135,7 @@ describe("pull_request_target event support", () => { const prompt = generatePrompt(envVars, mockGitHubData, false, "tag"); // Should include git commands for non-commit-signing mode - expect(prompt).toContain("git push"); + expect(prompt).toContain("scripts/git-push.sh origin"); expect(prompt).toContain( "Always push to the existing branch when triggered on a PR", ); diff --git a/test/trigger-validation.test.ts b/test/trigger-validation.test.ts index 2fccd04..35abf1e 100644 --- a/test/trigger-validation.test.ts +++ b/test/trigger-validation.test.ts @@ -35,6 +35,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, @@ -63,6 +64,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, @@ -275,6 +277,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, @@ -304,6 +307,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", }, @@ -333,6 +337,7 @@ describe("checkContainsTrigger", () => { labelTrigger: "", branchPrefix: "claude/", useStickyComment: false, + classifyInlineComments: true, useCommitSigning: false, allowedBots: "", },