From 4b26b332cbccd250986ef98eb8e129f55503b788 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:12:52 +0000 Subject: [PATCH 1/8] Initial plan From f4aa54a4e2f8020141db8c72c79842a8e17595ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:44:08 +0000 Subject: [PATCH 2/8] Add DeepWork Review GitHub Action - action.yml: composite action that installs Claude Code, uv, clones the deepwork plugin, runs /review in CI mode, commits all changes, and posts inline PR review comments - prompts/review.txt: CI-mode prompt (no AskUserQuestion, make all changes, write /tmp/deepwork_changes.json) - scripts/post-review-comments.py: posts GitHub PR review with one inline comment per changed file; falls back to diff stats if Claude's change summary is absent; uses base branch for robust diff detection - scripts/merge-mcp-config.py: safely merges deepwork MCP server entry into an existing .mcp.json (or creates one), backed up and restored - .github/workflows/example.yml: ready-to-copy example workflow - README.md: full usage docs, inputs table, prerequisites - .gitignore: exclude __pycache__ Agent-Logs-Url: https://github.com/Unsupervisedcom/deepwork-action/sessions/012963c8-b297-4729-9b7b-26919e945901 Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> --- .github/workflows/example.yml | 38 ++++ .gitignore | 1 + README.md | 89 +++++++- action.yml | 167 ++++++++++++++ prompts/review.txt | 38 ++++ .../merge-mcp-config.cpython-312.pyc | Bin 0 -> 1355 bytes .../post-review-comments.cpython-312.pyc | Bin 0 -> 8887 bytes scripts/merge-mcp-config.py | 29 +++ scripts/post-review-comments.py | 204 ++++++++++++++++++ 9 files changed, 565 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/example.yml create mode 100644 .gitignore create mode 100644 action.yml create mode 100644 prompts/review.txt create mode 100644 scripts/__pycache__/merge-mcp-config.cpython-312.pyc create mode 100644 scripts/__pycache__/post-review-comments.cpython-312.pyc create mode 100644 scripts/merge-mcp-config.py create mode 100644 scripts/post-review-comments.py diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml new file mode 100644 index 0000000..7bf3e77 --- /dev/null +++ b/.github/workflows/example.yml @@ -0,0 +1,38 @@ +name: DeepWork Review + +on: + pull_request: + types: [opened, synchronize, reopened] + +# Prevent concurrent runs on the same PR +concurrency: + group: deepwork-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deepwork-review: + runs-on: ubuntu-latest + # Required permissions + permissions: + contents: write # push auto-fix commits to the PR branch + pull-requests: write # post inline PR review comments + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + # Fetch full history so DeepWork can diff against the base branch + fetch-depth: 0 + # Use the merge ref so we operate on the PR's head commit + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run DeepWork Review + uses: Unsupervisedcom/deepwork-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + # Optional overrides: + # model: claude-sonnet-4-5 + # max_turns: '50' + # commit_message: 'chore: apply DeepWork review suggestions [skip ci]' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e30f246 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +scripts/__pycache__/ diff --git a/README.md b/README.md index f67f337..98f6e9c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,89 @@ # deepwork-action -DeepWork GitHub Action + +A prebuilt GitHub Action that runs [Claude Code](https://docs.anthropic.com/en/docs/claude-code) on a Pull Request with the [DeepWork](https://github.com/Unsupervisedcom/deepwork) plugin installed, triggers the `/review` skill, auto-commits all review-driven improvements back to the PR branch, and posts inline PR review comments explaining each change. + +## How It Works + +1. **DeepWork review** — Claude Code runs the `/review` skill, which reads your `.deepreview` config files to discover review rules, diffs the PR branch, and dispatches parallel review agents scoped to exactly the right files. +2. **Apply changes** — Claude applies every suggested improvement (bugs, style, performance, security, docs, refactoring) without asking for confirmation. +3. **Auto-commit** — All file changes are committed back to the PR branch under the `deepwork-action[bot]` identity. +4. **Inline PR comments** — A GitHub PR review is posted with one inline comment per changed file, describing what was changed and why, so your team can review each improvement. + +## Prerequisites + +1. **Anthropic API key** — add it as a repository secret named `ANTHROPIC_API_KEY`. +2. **`.deepreview` configuration** — place one or more `.deepreview` files in your repository defining your review rules. See the [DeepWork Reviews documentation](https://github.com/Unsupervisedcom/deepwork/blob/main/README_REVIEWS.md) for details. + +## Usage + +Create a workflow file such as `.github/workflows/deepwork-review.yml`: + +```yaml +name: DeepWork Review + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: deepwork-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deepwork-review: + runs-on: ubuntu-latest + permissions: + contents: write # push auto-fix commits to the PR branch + pull-requests: write # post inline PR review comments + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run DeepWork Review + uses: Unsupervisedcom/deepwork-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} +``` + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `anthropic_api_key` | ✅ | — | Anthropic API key for Claude Code | +| `github_token` | ✅ | — | GitHub token with `contents: write` and `pull-requests: write` | +| `model` | ❌ | `claude-sonnet-4-5` | Claude model to use | +| `max_turns` | ❌ | `50` | Maximum agentic turns for Claude Code | +| `commit_message` | ❌ | `chore: apply DeepWork review suggestions [skip ci]` | Commit message for auto-committed changes | + +## What Gets Changed + +The action applies **all** suggestions from your `.deepreview` rules, including: + +- Bug fixes and null-safety checks +- Style and formatting improvements +- Performance optimisations +- Security hardening +- Documentation updates +- Refactoring suggestions + +If no `.deepreview` rules are configured in the repository, the action exits cleanly without making any changes or commits. + +## Review Comments + +After pushing the auto-fix commit, the action posts a GitHub PR review with inline comments on each changed file. The comments appear in the **Files Changed** tab and describe what was changed and why, so your team can accept, request modifications, or revert individual changes as needed. + +## Security + +- The action runs Claude with `--dangerously-skip-permissions` in a sandboxed GitHub Actions runner. It has no access to secrets beyond what you explicitly provide. +- Auto-fix commits are signed with the `deepwork-action[bot]` identity. +- The `[skip ci]` suffix on the default commit message prevents the action from triggering itself recursively. + +## License + +See [LICENSE](LICENSE). diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..66370b4 --- /dev/null +++ b/action.yml @@ -0,0 +1,167 @@ +name: 'DeepWork Review Action' +description: 'Run DeepWork reviews via Claude Code on a PR, auto-commit improvements, and add inline PR review comments' +author: 'Unsupervisedcom' + +branding: + icon: 'check-circle' + color: 'blue' + +inputs: + anthropic_api_key: + description: 'Anthropic API key for Claude Code' + required: true + github_token: + description: 'GitHub token with write access to commit changes and post review comments' + required: true + model: + description: 'Claude model to use (e.g. claude-sonnet-4-5, claude-opus-4-5)' + required: false + default: 'claude-sonnet-4-5' + max_turns: + description: 'Maximum number of agentic turns for Claude Code' + required: false + default: '50' + commit_message: + description: 'Commit message for auto-committed review changes' + required: false + default: 'chore: apply DeepWork review suggestions [skip ci]' + +runs: + using: 'composite' + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Claude Code CLI + shell: bash + run: | + # Install the latest Claude Code CLI. + # Minimum safe version is 2.1.53 (fixes all known CVEs as of this action's release). + npm install -g @anthropic-ai/claude-code + # Verify the installed version meets the minimum requirement. + INSTALLED=$(claude --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + MAJOR=$(echo "$INSTALLED" | cut -d. -f1) + MINOR=$(echo "$INSTALLED" | cut -d. -f2) + PATCH=$(echo "$INSTALLED" | cut -d. -f3) + if [ "$MAJOR" -lt 2 ] || \ + ([ "$MAJOR" -eq 2 ] && [ "$MINOR" -lt 1 ]) || \ + ([ "$MAJOR" -eq 2 ] && [ "$MINOR" -eq 1 ] && [ "$PATCH" -lt 53 ]); then + echo "ERROR: claude-code $INSTALLED is below the minimum safe version 2.1.53" >&2 + exit 1 + fi + echo "claude-code $INSTALLED meets minimum version requirement." + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: 'latest' + + - name: Clone deepwork Claude plugin + shell: bash + run: | + PLUGIN_DIR="/tmp/deepwork-plugin" + git clone --depth 1 --filter=blob:none --sparse \ + https://github.com/Unsupervisedcom/deepwork.git \ + "$PLUGIN_DIR" + cd "$PLUGIN_DIR" + git sparse-checkout set plugins/claude + echo "Plugin ready at $PLUGIN_DIR/plugins/claude" + + - name: Fetch base branch for git diff + shell: bash + run: | + BASE_REF="${{ github.event.pull_request.base.ref }}" + if [ -n "$BASE_REF" ]; then + git fetch origin "$BASE_REF" --depth=1 || \ + echo "Warning: could not fetch base branch '$BASE_REF'; diff detection may be incomplete" + fi + + - name: Prepare MCP config + shell: bash + run: | + # Merge deepwork's MCP server into the project's .mcp.json (if any), or create one. + # We always restore the original file afterwards (see "Restore MCP config" step). + + if [ -f .mcp.json ]; then + cp .mcp.json /tmp/deepwork-original-mcp.json + else + touch /tmp/deepwork-no-original-mcp # flag: no original file existed + fi + + python3 "${{ github.action_path }}/scripts/merge-mcp-config.py" .mcp.json + + - name: Run DeepWork review with Claude Code + shell: bash + env: + ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + GH_TOKEN: ${{ inputs.github_token }} + run: | + PLUGIN_DIR="/tmp/deepwork-plugin/plugins/claude" + PROMPT_FILE="${{ github.action_path }}/prompts/review.txt" + + # Clean up any leftover changes file from a previous run. + rm -f /tmp/deepwork_changes.json + + # Run Claude Code with the deepwork plugin and the review prompt. + # + # --plugin-dir loads DeepWork skills, hooks, and MCP config + # --print non-interactive / CI mode + # --dangerously-skip-permissions suppress all permission prompts + claude \ + --print \ + --dangerously-skip-permissions \ + --model "${{ inputs.model }}" \ + --max-turns "${{ inputs.max_turns }}" \ + --plugin-dir "$PLUGIN_DIR" \ + < "$PROMPT_FILE" || true + + - name: Restore MCP config + if: always() + shell: bash + run: | + if [ -f /tmp/deepwork-original-mcp.json ]; then + cp /tmp/deepwork-original-mcp.json .mcp.json + elif [ -f /tmp/deepwork-no-original-mcp ]; then + rm -f .mcp.json + fi + + - name: Commit and push changes + id: commit + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + run: | + git config user.name "deepwork-action[bot]" + git config user.email "deepwork-action[bot]@users.noreply.github.com" + + # Check for any modified, added, or deleted tracked files + if git diff --quiet && git diff --cached --quiet \ + && [ -z "$(git ls-files --others --exclude-standard)" ]; then + echo "No changes to commit." + echo "changes_made=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "changes_made=true" >> "$GITHUB_OUTPUT" + + git add -A + git commit -m "${{ inputs.commit_message }}" + + # Authenticate push via the provided token. + REPO="${{ github.repository }}" + git remote set-url origin \ + "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO}.git" + git push + + - name: Post inline PR review comments + if: steps.commit.outputs.changes_made == 'true' && github.event.pull_request.number != '' + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + python3 "${{ github.action_path }}/scripts/post-review-comments.py" diff --git a/prompts/review.txt b/prompts/review.txt new file mode 100644 index 0000000..609f093 --- /dev/null +++ b/prompts/review.txt @@ -0,0 +1,38 @@ +/review + +You are running in a fully automated CI environment on a GitHub Pull Request. +There is NO human watching this session. Follow these critical rules at all times: + +## Automation Rules (MANDATORY) + +1. **NEVER use AskUserQuestion** — you are in CI mode; make every decision autonomously. +2. **Make ALL changes** suggested by the review findings — not just "obviously good" ones. + Apply every finding: bugs, style, performance, security, documentation, and refactoring. + When a finding offers multiple approaches, choose the best one yourself. +3. **Iterate** — after making changes, re-run the review until it comes back clean. +4. **If no `.deepreview` rules are configured** — output the message "No review rules configured." + and stop. Do not attempt to configure rules; that is the repository owner's responsibility. + +## Change Tracking (REQUIRED) + +For every file you modify, append an entry to `/tmp/deepwork_changes.json`. +Create the file with `{"changes": []}` if it does not yet exist. + +Each entry must follow this exact JSON structure: + +```json +{ + "file": "relative/path/to/changed/file", + "line": , + "description": "One-sentence description of what was changed", + "reason": "The review finding that prompted this change" +} +``` + +Write the final file when all changes are complete and the review passes. + +## Important + +- You have full permission to edit, create, and delete files in this repository. +- Do NOT commit changes yourself — the CI workflow handles git commit and push. +- Do NOT push changes yourself — the CI workflow handles git commit and push. diff --git a/scripts/__pycache__/merge-mcp-config.cpython-312.pyc b/scripts/__pycache__/merge-mcp-config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4dd6bb267bffab10036eb1ec121353d5d817e8e2 GIT binary patch literal 1355 zcmZuwO>7%Q6n?Wm-u2p{Nu89Sm}J@pMV9LH7okQ`dq|L?KublbP<^qsI}>Led)Jy> zC$>-FL_kP5r$hpz00)EuQn+zKLOpW9IBm+HN+3?Th2{XtiJ6U^s7mLsv+vFKzW2Rv zXMgGGu|TX(zklrZsQ`b=pu1w7m;Ij*z#7~G0y!XvC?%9b(XQkav8yGutmQNW3Q(OY z#blS@c;6HZauID|8h{N5r1oMY(Owa_F5WXFlIYzyXSk3vX`EUlHYsq_D84LtpI@7u zke!W7O{h5d{JD%o<Cvc@6&imd~T$ZF+0m?eQH|G|{D|I|svBUXn=*x6ed&9Ecr8Wto=CgO- zxgC*Rz&0VlMMf(n+o6YtV_j)u9YIx(2ytVA>29Pm7`tHE9>Gh@4LdcA2GvrCAK1p7>5a z&oyZ+SA%-M4e46ReZa(j2wW{uFXCS0rr^%YL#E3Ujkr$8Pv!Hr=lP)>y1o|(rdTvc z!1aSRs z`Eus#o-OnnzWmX(rn+XXn(KY5iS0!Chw#U@+HcG(n}7DDmd%yXHl&`Yi4SjnaC2Mj z-&0im%#+ls&1-8Ds}t+Y;$^Tc6D~WwuQF)uWY^7>c74X6TgGLVm1ex)~~UlN09z+F9pfo ooy15hG4c$M>^<Y8lnD>bzNxhOa0rR#|I;L2hyVZp literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/post-review-comments.cpython-312.pyc b/scripts/__pycache__/post-review-comments.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..145078cb02dba828936a5da9992e6cb5f1136d72 GIT binary patch literal 8887 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 X2yjlOAF$j0ljHyVG+YU;o>Kci!6)Y` literal 0 HcmV?d00001 diff --git a/scripts/merge-mcp-config.py b/scripts/merge-mcp-config.py new file mode 100644 index 0000000..3ee0f21 --- /dev/null +++ b/scripts/merge-mcp-config.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +""" +Merge the deepwork MCP server entry into an existing .mcp.json file. + +Usage: python3 merge-mcp-config.py + +Reads the JSON file, adds/replaces the 'deepwork' entry under 'mcpServers', +and writes the result back to the same file. +""" +import json +import sys +from pathlib import Path + +mcp_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".mcp.json") + +existing: dict = {} +if mcp_path.exists(): + try: + existing = json.loads(mcp_path.read_text()) + except json.JSONDecodeError: + existing = {} + +existing.setdefault("mcpServers", {})["deepwork"] = { + "command": "uvx", + "args": ["deepwork", "serve", "--platform", "claude"], +} + +mcp_path.write_text(json.dumps(existing, indent=2) + "\n") +print(f"Written {mcp_path}") diff --git a/scripts/post-review-comments.py b/scripts/post-review-comments.py new file mode 100644 index 0000000..7308abf --- /dev/null +++ b/scripts/post-review-comments.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +Post inline PR review comments for each file changed by the DeepWork review. + +Reads /tmp/deepwork_changes.json (written by Claude) for per-change descriptions. +Falls back to a diff-based summary when the file is absent or an entry is missing. +Posts a single GitHub PR review with one inline comment per changed file. +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess: + return subprocess.run(cmd, capture_output=True, text=True, **kwargs) + + +def get_head_sha() -> str: + return run(["git", "rev-parse", "HEAD"]).stdout.strip() + + +def get_changed_files(base_ref: str) -> list[str]: + """Return files changed between the base branch and HEAD.""" + # Try the exact remote ref first (available when the base branch was fetched). + for ref in (f"origin/{base_ref}", base_ref, "HEAD~1"): + result = run(["git", "diff", ref, "HEAD", "--name-only", "--diff-filter=ACMR"]) + if result.returncode == 0 and result.stdout.strip(): + return [f for f in result.stdout.splitlines() if f.strip()] + return [] + + +def get_diff(file_path: str, base_ref: str) -> str: + for ref in (f"origin/{base_ref}", base_ref, "HEAD~1"): + result = run(["git", "diff", ref, "HEAD", "--", file_path]) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout + return "" + + +def first_changed_line(diff: str) -> int: + """ + Return the line number (in the new file) of the first added line. + Parses unified diff hunk headers: @@ -old +new,count @@ + """ + current_new = 0 + for line in diff.splitlines(): + if line.startswith("@@"): + m = re.search(r"\+(\d+)", line) + if m: + current_new = int(m.group(1)) + elif line.startswith("+") and not line.startswith("+++"): + return max(current_new, 1) + elif not line.startswith("-") and not line.startswith("\\"): + current_new += 1 + return 1 + + +def count_added_lines(diff: str) -> int: + return sum( + 1 + for line in diff.splitlines() + if line.startswith("+") and not line.startswith("+++") + ) + + +# --------------------------------------------------------------------------- +# Load Claude's change summary (optional) +# --------------------------------------------------------------------------- + +def load_changes_by_file() -> dict[str, list[dict[str, Any]]]: + changes_path = Path("/tmp/deepwork_changes.json") + if not changes_path.exists(): + return {} + try: + data = json.loads(changes_path.read_text()) + by_file: dict[str, list[dict]] = {} + for entry in data.get("changes", []): + fp = entry.get("file", "").lstrip("./") + by_file.setdefault(fp, []).append(entry) + return by_file + except (json.JSONDecodeError, OSError) as exc: + print(f"Warning: could not parse /tmp/deepwork_changes.json: {exc}", file=sys.stderr) + return {} + + +# --------------------------------------------------------------------------- +# Build comment body for a file +# --------------------------------------------------------------------------- + +def build_comment_body( + file_path: str, + diff: str, + changes: list[dict[str, Any]], +) -> str: + if changes: + bullets = "\n".join( + f"- **{c.get('description', 'Change applied')}**" + + (f"\n *{c.get('reason', '')}*" if c.get("reason") else "") + for c in changes + ) + return ( + "🤖 **DeepWork Review** applied the following changes:\n\n" + + bullets + ) + # Fallback: diff statistics + added = count_added_lines(diff) + return ( + f"🤖 **DeepWork Review** applied {added} line(s) of changes to this file " + f"based on review findings." + ) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> None: + pr_number = os.environ.get("PR_NUMBER", "") + repo = os.environ.get("GITHUB_REPOSITORY", "") + # GITHUB_BASE_REF is set automatically by GitHub Actions for pull_request events. + base_ref = os.environ.get("GITHUB_BASE_REF", "main") + + if not pr_number or not repo: + print("PR_NUMBER or GITHUB_REPOSITORY not set; skipping review comments.", file=sys.stderr) + sys.exit(0) + + commit_sha = get_head_sha() + changed_files = get_changed_files(base_ref) + + if not changed_files: + print("No changed files found between base branch and HEAD; nothing to comment on.") + sys.exit(0) + + changes_by_file = load_changes_by_file() + + inline_comments: list[dict[str, Any]] = [] + for file_path in changed_files: + diff = get_diff(file_path, base_ref) + if not diff.strip(): + continue + + line_number = first_changed_line(diff) + normalised = file_path.lstrip("./") + file_changes = changes_by_file.get(normalised, []) or changes_by_file.get(file_path, []) + body = build_comment_body(file_path, diff, file_changes) + + inline_comments.append({ + "path": file_path, + "line": line_number, + "side": "RIGHT", + "body": body, + }) + + if not inline_comments: + print("No inline comments to post.") + sys.exit(0) + + # Build the review payload + review_body = ( + f"🤖 **DeepWork automated review** applied changes to " + f"{len(inline_comments)} file(s).\n\n" + "Review the inline comments below for details on each change." + ) + review_payload: dict[str, Any] = { + "commit_id": commit_sha, + "body": review_body, + "event": "COMMENT", + "comments": inline_comments, + } + + payload_path = Path("/tmp/deepwork_review_payload.json") + payload_path.write_text(json.dumps(review_payload, indent=2)) + + result = run([ + "gh", "api", + f"repos/{repo}/pulls/{pr_number}/reviews", + "--method", "POST", + "--input", str(payload_path), + ]) + + if result.returncode != 0: + print(f"Error posting PR review: {result.stderr}", file=sys.stderr) + # Non-fatal: the changes are already committed; just warn. + sys.exit(0) + + print( + f"Posted PR review with {len(inline_comments)} inline comment(s) " + f"on PR #{pr_number}." + ) + + +if __name__ == "__main__": + main() From 5c3552d814db957bfca7a69f4c36354f605f56ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:44:30 +0000 Subject: [PATCH 3/8] Remove accidentally committed __pycache__ files Agent-Logs-Url: https://github.com/Unsupervisedcom/deepwork-action/sessions/012963c8-b297-4729-9b7b-26919e945901 Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> --- .../__pycache__/merge-mcp-config.cpython-312.pyc | Bin 1355 -> 0 bytes .../post-review-comments.cpython-312.pyc | Bin 8887 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 scripts/__pycache__/merge-mcp-config.cpython-312.pyc delete mode 100644 scripts/__pycache__/post-review-comments.cpython-312.pyc diff --git a/scripts/__pycache__/merge-mcp-config.cpython-312.pyc b/scripts/__pycache__/merge-mcp-config.cpython-312.pyc deleted file mode 100644 index 4dd6bb267bffab10036eb1ec121353d5d817e8e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1355 zcmZuwO>7%Q6n?Wm-u2p{Nu89Sm}J@pMV9LH7okQ`dq|L?KublbP<^qsI}>Led)Jy> zC$>-FL_kP5r$hpz00)EuQn+zKLOpW9IBm+HN+3?Th2{XtiJ6U^s7mLsv+vFKzW2Rv zXMgGGu|TX(zklrZsQ`b=pu1w7m;Ij*z#7~G0y!XvC?%9b(XQkav8yGutmQNW3Q(OY z#blS@c;6HZauID|8h{N5r1oMY(Owa_F5WXFlIYzyXSk3vX`EUlHYsq_D84LtpI@7u zke!W7O{h5d{JD%o<Cvc@6&imd~T$ZF+0m?eQH|G|{D|I|svBUXn=*x6ed&9Ecr8Wto=CgO- zxgC*Rz&0VlMMf(n+o6YtV_j)u9YIx(2ytVA>29Pm7`tHE9>Gh@4LdcA2GvrCAK1p7>5a z&oyZ+SA%-M4e46ReZa(j2wW{uFXCS0rr^%YL#E3Ujkr$8Pv!Hr=lP)>y1o|(rdTvc z!1aSRs z`Eus#o-OnnzWmX(rn+XXn(KY5iS0!Chw#U@+HcG(n}7DDmd%yXHl&`Yi4SjnaC2Mj z-&0im%#+ls&1-8Ds}t+Y;$^Tc6D~WwuQF)uWY^7>c74X6TgGLVm1ex)~~UlN09z+F9pfo ooy15hG4c$M>^<Y8lnD>bzNxhOa0rR#|I;L2hyVZp diff --git a/scripts/__pycache__/post-review-comments.cpython-312.pyc b/scripts/__pycache__/post-review-comments.cpython-312.pyc deleted file mode 100644 index 145078cb02dba828936a5da9992e6cb5f1136d72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8887 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 X2yjlOAF$j0ljHyVG+YU;o>Kci!6)Y` From 654752cbc783fdf95a0b64d84d4c15d8c4df28a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:41:28 +0000 Subject: [PATCH 4/8] Use Node 24 + setup-node@v6, add .deepwork/tmp cache per PR - Upgrade setup-node from v4/node-20 to v6/node-24 - Add actions/cache step to persist .deepwork/tmp across runs on the same PR, using a run-id suffix so the cache is always updated and restore-keys picks up the latest prior entry for the same PR number Agent-Logs-Url: https://github.com/Unsupervisedcom/deepwork-action/sessions/1f9e2774-9151-4bc9-8826-569167d36ad3 Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> --- action.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 66370b4..cebdf4a 100644 --- a/action.yml +++ b/action.yml @@ -30,9 +30,9 @@ runs: using: 'composite' steps: - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '24' - name: Install Claude Code CLI shell: bash @@ -69,6 +69,17 @@ runs: git sparse-checkout set plugins/claude echo "Plugin ready at $PLUGIN_DIR/plugins/claude" + - name: Restore DeepWork review cache + uses: actions/cache@v4 + with: + path: .deepwork/tmp + # A unique save key per run ensures the cache is always updated with the + # latest review state after each run. restore-keys picks up the most recent + # cache for this PR so already-passed reviews are not re-run. + key: deepwork-review-pr-${{ github.event.pull_request.number }}-${{ github.run_id }} + restore-keys: | + deepwork-review-pr-${{ github.event.pull_request.number }}- + - name: Fetch base branch for git diff shell: bash run: | From bc66f07c4006c8a867f6edd21ca6f2045f0e84b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:51:17 +0000 Subject: [PATCH 5/8] Use anthropics/claude-code-base-action; proper plugin install; remove MCP config hack - Replace npm + sparse-clone + --plugin-dir approach with the official anthropics/claude-code-base-action@beta, which installs Claude Code via curl and handles plugin installation natively through its plugin_marketplaces and plugins inputs - Remove "Set up Node.js" step (no longer needed; base action manages it) - Remove "Install Claude Code CLI" step (base action installs via curl) - Remove "Clone deepwork Claude plugin" step; replaced by: plugin_marketplaces: https://github.com/Unsupervisedcom/deepwork.git plugins: deepwork@deepwork-plugins - Remove "Prepare MCP config" and "Restore MCP config" steps (proper plugin install handles MCP config automatically) - Delete scripts/merge-mcp-config.py (no longer needed) - Update README to reflect new architecture and mention caching Agent-Logs-Url: https://github.com/Unsupervisedcom/deepwork-action/sessions/9b5cd224-5209-435a-b81f-b5a83cd55f8b Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> --- README.md | 16 ++++--- action.yml | 90 ++++++------------------------------- scripts/merge-mcp-config.py | 29 ------------ 3 files changed, 24 insertions(+), 111 deletions(-) delete mode 100644 scripts/merge-mcp-config.py diff --git a/README.md b/README.md index 98f6e9c..3b71c75 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ A prebuilt GitHub Action that runs [Claude Code](https://docs.anthropic.com/en/d ## How It Works -1. **DeepWork review** — Claude Code runs the `/review` skill, which reads your `.deepreview` config files to discover review rules, diffs the PR branch, and dispatches parallel review agents scoped to exactly the right files. -2. **Apply changes** — Claude applies every suggested improvement (bugs, style, performance, security, docs, refactoring) without asking for confirmation. -3. **Auto-commit** — All file changes are committed back to the PR branch under the `deepwork-action[bot]` identity. -4. **Inline PR comments** — A GitHub PR review is posted with one inline comment per changed file, describing what was changed and why, so your team can review each improvement. +1. **DeepWork plugin install** — The action installs the DeepWork plugin from the marketplace using Claude Code's native plugin system, loading all review skills, hooks, and MCP server configuration automatically. +2. **DeepWork review** — Claude Code runs the `/review` skill, which reads your `.deepreview` config files to discover review rules, diffs the PR branch, and dispatches parallel review agents scoped to exactly the right files. +3. **Apply changes** — Claude applies every suggested improvement (bugs, style, performance, security, docs, refactoring) without asking for confirmation. +4. **Auto-commit** — All file changes are committed back to the PR branch under the `deepwork-action[bot]` identity. +5. **Inline PR comments** — A GitHub PR review is posted with one inline comment per changed file, describing what was changed and why, so your team can review each improvement. ## Prerequisites @@ -78,9 +79,14 @@ If no `.deepreview` rules are configured in the repository, the action exits cle After pushing the auto-fix commit, the action posts a GitHub PR review with inline comments on each changed file. The comments appear in the **Files Changed** tab and describe what was changed and why, so your team can accept, request modifications, or revert individual changes as needed. +## Caching + +Review state is cached per PR using GitHub Actions cache, keyed on the PR number. This means already-passed reviews are not re-run when you push new commits to the same PR — only code that has changed since the last review is re-evaluated. + ## Security -- The action runs Claude with `--dangerously-skip-permissions` in a sandboxed GitHub Actions runner. It has no access to secrets beyond what you explicitly provide. +- Claude Code is installed and run via the official [`anthropics/claude-code-base-action`](https://github.com/anthropics/claude-code-base-action). +- The action runs with `--dangerously-skip-permissions` in a sandboxed GitHub Actions runner. It has no access to secrets beyond what you explicitly provide. - Auto-fix commits are signed with the `deepwork-action[bot]` identity. - The `[skip ci]` suffix on the default commit message prevents the action from triggering itself recursively. diff --git a/action.yml b/action.yml index cebdf4a..5b2be34 100644 --- a/action.yml +++ b/action.yml @@ -29,46 +29,11 @@ inputs: runs: using: 'composite' steps: - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Install Claude Code CLI - shell: bash - run: | - # Install the latest Claude Code CLI. - # Minimum safe version is 2.1.53 (fixes all known CVEs as of this action's release). - npm install -g @anthropic-ai/claude-code - # Verify the installed version meets the minimum requirement. - INSTALLED=$(claude --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) - MAJOR=$(echo "$INSTALLED" | cut -d. -f1) - MINOR=$(echo "$INSTALLED" | cut -d. -f2) - PATCH=$(echo "$INSTALLED" | cut -d. -f3) - if [ "$MAJOR" -lt 2 ] || \ - ([ "$MAJOR" -eq 2 ] && [ "$MINOR" -lt 1 ]) || \ - ([ "$MAJOR" -eq 2 ] && [ "$MINOR" -eq 1 ] && [ "$PATCH" -lt 53 ]); then - echo "ERROR: claude-code $INSTALLED is below the minimum safe version 2.1.53" >&2 - exit 1 - fi - echo "claude-code $INSTALLED meets minimum version requirement." - - name: Install uv uses: astral-sh/setup-uv@v5 with: version: 'latest' - - name: Clone deepwork Claude plugin - shell: bash - run: | - PLUGIN_DIR="/tmp/deepwork-plugin" - git clone --depth 1 --filter=blob:none --sparse \ - https://github.com/Unsupervisedcom/deepwork.git \ - "$PLUGIN_DIR" - cd "$PLUGIN_DIR" - git sparse-checkout set plugins/claude - echo "Plugin ready at $PLUGIN_DIR/plugins/claude" - - name: Restore DeepWork review cache uses: actions/cache@v4 with: @@ -89,54 +54,25 @@ runs: echo "Warning: could not fetch base branch '$BASE_REF'; diff detection may be incomplete" fi - - name: Prepare MCP config + - name: Prepare review run shell: bash run: | - # Merge deepwork's MCP server into the project's .mcp.json (if any), or create one. - # We always restore the original file afterwards (see "Restore MCP config" step). - - if [ -f .mcp.json ]; then - cp .mcp.json /tmp/deepwork-original-mcp.json - else - touch /tmp/deepwork-no-original-mcp # flag: no original file existed - fi - - python3 "${{ github.action_path }}/scripts/merge-mcp-config.py" .mcp.json + # Clean up any leftover changes file from a previous run. + rm -f /tmp/deepwork_changes.json - name: Run DeepWork review with Claude Code - shell: bash + uses: anthropics/claude-code-base-action@beta env: - ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} GH_TOKEN: ${{ inputs.github_token }} - run: | - PLUGIN_DIR="/tmp/deepwork-plugin/plugins/claude" - PROMPT_FILE="${{ github.action_path }}/prompts/review.txt" - - # Clean up any leftover changes file from a previous run. - rm -f /tmp/deepwork_changes.json - - # Run Claude Code with the deepwork plugin and the review prompt. - # - # --plugin-dir loads DeepWork skills, hooks, and MCP config - # --print non-interactive / CI mode - # --dangerously-skip-permissions suppress all permission prompts - claude \ - --print \ - --dangerously-skip-permissions \ - --model "${{ inputs.model }}" \ - --max-turns "${{ inputs.max_turns }}" \ - --plugin-dir "$PLUGIN_DIR" \ - < "$PROMPT_FILE" || true - - - name: Restore MCP config - if: always() - shell: bash - run: | - if [ -f /tmp/deepwork-original-mcp.json ]; then - cp /tmp/deepwork-original-mcp.json .mcp.json - elif [ -f /tmp/deepwork-no-original-mcp ]; then - rm -f .mcp.json - fi + with: + anthropic_api_key: ${{ inputs.anthropic_api_key }} + prompt_file: ${{ github.action_path }}/prompts/review.txt + plugin_marketplaces: https://github.com/Unsupervisedcom/deepwork.git + plugins: deepwork@deepwork-plugins + claude_args: >- + --dangerously-skip-permissions + --model ${{ inputs.model }} + --max-turns ${{ inputs.max_turns }} - name: Commit and push changes id: commit diff --git a/scripts/merge-mcp-config.py b/scripts/merge-mcp-config.py deleted file mode 100644 index 3ee0f21..0000000 --- a/scripts/merge-mcp-config.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -""" -Merge the deepwork MCP server entry into an existing .mcp.json file. - -Usage: python3 merge-mcp-config.py - -Reads the JSON file, adds/replaces the 'deepwork' entry under 'mcpServers', -and writes the result back to the same file. -""" -import json -import sys -from pathlib import Path - -mcp_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".mcp.json") - -existing: dict = {} -if mcp_path.exists(): - try: - existing = json.loads(mcp_path.read_text()) - except json.JSONDecodeError: - existing = {} - -existing.setdefault("mcpServers", {})["deepwork"] = { - "command": "uvx", - "args": ["deepwork", "serve", "--platform", "claude"], -} - -mcp_path.write_text(json.dumps(existing, indent=2) + "\n") -print(f"Written {mcp_path}") From c68281073336a852d99dd3a9d8f5180025b6d194 Mon Sep 17 00:00:00 2001 From: Noah Horton Date: Wed, 8 Apr 2026 15:53:39 -0600 Subject: [PATCH 6/8] Apply suggestion from @nhorton --- .github/workflows/example.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml index 7bf3e77..c408dc9 100644 --- a/.github/workflows/example.yml +++ b/.github/workflows/example.yml @@ -22,6 +22,7 @@ jobs: uses: actions/checkout@v4 with: # Fetch full history so DeepWork can diff against the base branch + # Set too `100` or something high but safe if you have a huge git history fetch-depth: 0 # Use the merge ref so we operate on the PR's head commit ref: ${{ github.event.pull_request.head.ref }} From f1a5126ce8a8eab159592905ed5f9f4353e2d841 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:04:41 +0000 Subject: [PATCH 7/8] Update model to claude-opus-4-6; replace [skip ci] with bot identity guard - Change default model to claude-opus-4-6 in action.yml, README, and example.yml - Remove [skip ci] from default commit_message; instead add `if: github.actor != 'deepwork-action[bot]'` guard to example workflow job so the action never re-triggers on its own commits - Add "THIS IS A MAJOR TOKEN COST SAVER!!!" to README caching section - Update README security section to describe the bot-identity approach Agent-Logs-Url: https://github.com/Unsupervisedcom/deepwork-action/sessions/799d7276-12e1-4540-9650-c480b0a44b83 Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> --- .github/workflows/example.yml | 6 ++++-- README.md | 11 ++++++----- action.yml | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml index c408dc9..55a3680 100644 --- a/.github/workflows/example.yml +++ b/.github/workflows/example.yml @@ -12,6 +12,8 @@ concurrency: jobs: deepwork-review: runs-on: ubuntu-latest + # Don't re-run on commits pushed by the action itself + if: github.actor != 'deepwork-action[bot]' # Required permissions permissions: contents: write # push auto-fix commits to the PR branch @@ -34,6 +36,6 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} # Optional overrides: - # model: claude-sonnet-4-5 + # model: claude-opus-4-6 # max_turns: '50' - # commit_message: 'chore: apply DeepWork review suggestions [skip ci]' + # commit_message: 'chore: apply DeepWork review suggestions' diff --git a/README.md b/README.md index 3b71c75..09ed05b 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ concurrency: jobs: deepwork-review: runs-on: ubuntu-latest + # Don't re-run on commits pushed by the action itself + if: github.actor != 'deepwork-action[bot]' permissions: contents: write # push auto-fix commits to the PR branch pull-requests: write # post inline PR review comments @@ -58,9 +60,9 @@ jobs: |-------|----------|---------|-------------| | `anthropic_api_key` | ✅ | — | Anthropic API key for Claude Code | | `github_token` | ✅ | — | GitHub token with `contents: write` and `pull-requests: write` | -| `model` | ❌ | `claude-sonnet-4-5` | Claude model to use | +| `model` | ❌ | `claude-opus-4-6` | Claude model to use | | `max_turns` | ❌ | `50` | Maximum agentic turns for Claude Code | -| `commit_message` | ❌ | `chore: apply DeepWork review suggestions [skip ci]` | Commit message for auto-committed changes | +| `commit_message` | ❌ | `chore: apply DeepWork review suggestions` | Commit message for auto-committed changes | ## What Gets Changed @@ -81,14 +83,13 @@ After pushing the auto-fix commit, the action posts a GitHub PR review with inli ## Caching -Review state is cached per PR using GitHub Actions cache, keyed on the PR number. This means already-passed reviews are not re-run when you push new commits to the same PR — only code that has changed since the last review is re-evaluated. +Review state is cached per PR using GitHub Actions cache, keyed on the PR number. This means already-passed reviews are not re-run when you push new commits to the same PR — only code that has changed since the last review is re-evaluated. THIS IS A MAJOR TOKEN COST SAVER!!! ## Security - Claude Code is installed and run via the official [`anthropics/claude-code-base-action`](https://github.com/anthropics/claude-code-base-action). - The action runs with `--dangerously-skip-permissions` in a sandboxed GitHub Actions runner. It has no access to secrets beyond what you explicitly provide. -- Auto-fix commits are signed with the `deepwork-action[bot]` identity. -- The `[skip ci]` suffix on the default commit message prevents the action from triggering itself recursively. +- Auto-fix commits are pushed under the `deepwork-action[bot]` identity. The example workflow includes `if: github.actor != 'deepwork-action[bot]'` at the job level so the action never triggers itself recursively. ## License diff --git a/action.yml b/action.yml index 5b2be34..f81fca9 100644 --- a/action.yml +++ b/action.yml @@ -14,9 +14,9 @@ inputs: description: 'GitHub token with write access to commit changes and post review comments' required: true model: - description: 'Claude model to use (e.g. claude-sonnet-4-5, claude-opus-4-5)' + description: 'Claude model to use (e.g. claude-sonnet-4-6, claude-opus-4-6)' required: false - default: 'claude-sonnet-4-5' + default: 'claude-opus-4-6' max_turns: description: 'Maximum number of agentic turns for Claude Code' required: false @@ -24,7 +24,7 @@ inputs: commit_message: description: 'Commit message for auto-committed review changes' required: false - default: 'chore: apply DeepWork review suggestions [skip ci]' + default: 'chore: apply DeepWork review suggestions' runs: using: 'composite' From 1290b16a3dd0e26fc7466c0b372db37f3b8456e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:15:43 +0000 Subject: [PATCH 8/8] Simplify pull_request trigger types: drop reopened, keep opened+synchronize Agent-Logs-Url: https://github.com/Unsupervisedcom/deepwork-action/sessions/bfca66ea-ca2d-447c-a7d7-f829f2ee6ad4 Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> --- .github/workflows/example.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/example.yml b/.github/workflows/example.yml index 55a3680..5124f58 100644 --- a/.github/workflows/example.yml +++ b/.github/workflows/example.yml @@ -2,7 +2,7 @@ name: DeepWork Review on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize] # Prevent concurrent runs on the same PR concurrency: diff --git a/README.md b/README.md index 09ed05b..e1e27a2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ name: DeepWork Review on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize] concurrency: group: deepwork-review-${{ github.event.pull_request.number }}