diff --git a/.github/workflows/pi-multi-review.yml b/.github/workflows/pi-multi-review.yml new file mode 100644 index 0000000..7373484 --- /dev/null +++ b/.github/workflows/pi-multi-review.yml @@ -0,0 +1,34 @@ +name: Pi Multi-Review + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + issue_comment: + types: [created] + +jobs: + multi-review: + # Skip draft PRs and cross-repo PRs; only trigger on /multi-review comments + if: > + (github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) || + (github.event_name == 'issue_comment' && github.event.issue.pull_request != null && startsWith(github.event.comment.body, '/multi-review')) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + steps: + - name: Checkout PR head + uses: actions/checkout@v6 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || github.event.issue.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref || github.event.issue.pull_request.head.ref }} + + - name: Run Pi Multi-Review + uses: ./pi-multi-review + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + provider: ${{ vars.PI_PROVIDER || 'anthropic' }} + model: ${{ vars.PI_MODEL || 'claude-sonnet-4-6' }} + api-key: ${{ secrets.PI_API_KEY }} + language: Chinese diff --git a/README.md b/README.md index c9cc426..f6bfa58 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ npx skills add sun-praise/opencode-actions - `architect-review`: architecture-level PR review focusing on coupling, layering, and structural concerns - `feature-missing`: audits PR implementation against linked issue spec to find missing features - `spec-coverage`: cross-references project spec/task files against PR implementation to find planned but unimplemented features +- `pi-multi-review`: multi-agent parallel PR review using pi-coding-agent-action with pi-parallel-agents team mode - `github-run-opencode`: one-step wrapper for the common `opencode github run` workflow - `setup-opencode`: installs OpenCode, restores a dedicated cache, and exports the binary path - `run-opencode`: runs `opencode` with optional retry logic for flaky GitHub network failures @@ -150,6 +151,7 @@ Unlike `feature-missing` (which checks PR self-described scope), `spec-coverage` | `architect-review` | PR diff + project conventions | Coupling, layering, module placement, structural concerns | | `feature-missing` | PR title/body + linked issues | PR self-described scope completeness | | `spec-coverage` | Project spec/task files | Full planned scope vs implementation | +| `pi-multi-review` | PR diff (via pi-agent) | Multi-reviewer parallel review: quality, security, performance, architecture | ## setup-opencode @@ -205,6 +207,7 @@ uses: sun-praise/opencode-actions/review@v2 uses: sun-praise/opencode-actions/architect-review@v2 uses: sun-praise/opencode-actions/feature-missing@v2 uses: sun-praise/opencode-actions/spec-coverage@v2 +uses: sun-praise/opencode-actions/pi-multi-review@v2 uses: sun-praise/opencode-actions/github-run-opencode@v2 uses: sun-praise/opencode-actions/setup-opencode@v2 uses: sun-praise/opencode-actions/run-opencode@v2 diff --git a/pi-multi-review/README.md b/pi-multi-review/README.md new file mode 100644 index 0000000..80be6e2 --- /dev/null +++ b/pi-multi-review/README.md @@ -0,0 +1,130 @@ +# Pi Multi-Review Action + +Multi-agent parallel PR review using [pi-coding-agent-action](https://github.com/shaftoe/pi-coding-agent-action) with [pi-parallel-agents](https://github.com/messense/pi-parallel-agents) team mode. + +Multiple reviewer agents (quality, security, performance, architecture) review your PR simultaneously, then a synthesizer agent produces a single consolidated review comment. + +## Usage + +```yaml +name: Pi Multi-Review + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + issue_comment: + types: [created] + +jobs: + multi-review: + if: > + (github.event_name == 'pull_request' && github.event.pull_request.draft == false) || + (github.event_name == 'issue_comment' && github.event.issue.pull_request != null && startsWith(github.event.comment.body, '/multi-review')) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + steps: + - uses: actions/checkout@v6 + + - uses: sun-praise/opencode-actions/pi-multi-review@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + provider: anthropic + model: claude-sonnet-4-6 + api-key: ${{ secrets.PI_API_KEY }} + language: Chinese +``` + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `github-token` | Yes | - | GitHub token for API access | +| `provider` | Yes | - | LLM provider (openai, anthropic, google, etc.) | +| `model` | No | `claude-sonnet-4-6` | Default model for all reviewers | +| `api-key` | No | - | LLM provider API key | +| `reviewers-config` | No | `""` | Inline YAML to override default reviewer personas | +| `max-tokens` | No | `4096` | Max tokens per reviewer response | +| `diff-max-lines` | No | `2000` | Max diff lines included in review | +| `diff-ignore-patterns` | No | `""` | File patterns to exclude from diffs | +| `base-url` | No | `""` | Provider base URL override (for proxies) | +| `thinking-level` | No | `medium` | Model thinking level (off, low, medium, high) | +| `language` | No | `Chinese` | Output language for reviews | + +## Default Reviewers + +When no `reviewers-config` is provided, 4 built-in reviewers run in parallel: + +| Reviewer | Focus | +|----------|-------| +| **quality** | Readability, naming, error handling, DRY, dead code | +| **security** | Input validation, injection, OWASP Top 10, secrets exposure | +| **performance** | Algorithmic complexity, N+1 queries, memory, caching | +| **architecture** | Coupling, separation of concerns, API design, scalability | + +A **synthesizer** agent merges all findings into one structured PR comment with: +- Merge decision (可合并 / 有条件合并 / 不可合并) +- Critical issues, warnings, suggestions +- Cross-validated findings (flagged by multiple reviewers) + +## Custom Reviewers + +Provide your own reviewers via `reviewers-config`: + +```yaml +- uses: sun-praise/opencode-actions/pi-multi-review@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + provider: anthropic + api-key: ${{ secrets.PI_API_KEY }} + reviewers-config: | + reviewers: + - name: security + role: Security Expert + prompt: | + Focus only on security vulnerabilities in this diff: + {{diff}} + + Report critical, warning, and info findings. + - name: testing + role: Test Coverage Analyst + prompt: | + Check test coverage for changes in this diff: + {{diff}} + + Are new code paths adequately tested? + synthesizer: + name: synthesizer + role: Review Synthesizer + prompt: | + Combine these findings into a structured review: + {task:security-review} + {task:testing-review} + Output as markdown. +``` + +## Template Variables + +Reviewer prompts support these placeholders: + +| Variable | Description | +|----------|-------------| +| `{{diff}}` | Full PR diff | +| `{{title}}` | PR title | +| `{{body}}` | PR description | +| `{{files}}` | List of changed files | +| `{{language}}` | Configured output language | + +Synthesizer prompts can reference reviewer outputs with `{task:-review}`. + +## Requirements + +- Linux runner (GitHub Actions `ubuntu-latest`) +- Python 3 with PyYAML (pre-installed on GitHub-hosted runners) +- A valid LLM API key for the configured provider + +## Security Note + +This action delegates to `shaftoe/pi-coding-agent-action@v2`. For production use, consider pinning to a specific commit SHA instead of the `v2` tag to protect against supply-chain attacks. See the [GitHub Actions security guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions). diff --git a/pi-multi-review/action.yml b/pi-multi-review/action.yml new file mode 100644 index 0000000..592e625 --- /dev/null +++ b/pi-multi-review/action.yml @@ -0,0 +1,93 @@ +name: Pi Multi-Review +description: Multi-agent parallel PR review using pi-coding-agent-action with pi-parallel-agents team mode. + +inputs: + github-token: + description: GitHub token for API access. + required: true + provider: + description: LLM provider (e.g. openai, anthropic, google). + required: true + model: + description: Default model for all reviewers (e.g. claude-sonnet-4-6, gpt-4o). + required: false + default: "claude-sonnet-4-6" + api-key: + description: API key for the LLM provider. Can also be set via provider-specific env vars. + required: false + default: "" + reviewers-config: + description: >- + Inline YAML string defining custom reviewer personas. When empty, uses the 4 built-in + reviewers (quality, security, performance, architecture). Each reviewer has: name, role, + prompt, model (optional). + required: false + default: "" + max-tokens: + description: Maximum tokens per reviewer response. + required: false + default: "4096" + diff-max-lines: + description: Maximum number of diff lines to include in PR diff. + required: false + default: "2000" + diff-ignore-patterns: + description: Space-separated file patterns to exclude from PR diffs. + required: false + default: "" + base-url: + description: Optional override for the provider base URL (e.g. for proxies or compatible gateways). + required: false + default: "" + thinking-level: + description: Thinking level for the model (off, low, medium, high). + required: false + default: "medium" + language: + description: Language for review output (e.g. Chinese, English). + required: false + default: "Chinese" + +runs: + using: composite + steps: + - if: ${{ runner.os != 'Linux' }} + shell: bash + run: | + set -euo pipefail + printf 'pi-multi-review currently supports Linux runners only\n' >&2 + exit 1 + + - name: Build multi-review prompt + id: build-prompt + shell: bash + env: + INPUT_REVIEWERS_CONFIG: ${{ inputs.reviewers-config }} + INPUT_MODEL: ${{ inputs.model }} + INPUT_LANGUAGE: ${{ inputs.language }} + INPUT_MAX_TOKENS: ${{ inputs.max-tokens }} + ACTION_PATH: ${{ github.action_path }} + run: | + set -euo pipefail + python3 "$ACTION_PATH/scripts/build-prompt.py" \ + --reviewers-config "$INPUT_REVIEWERS_CONFIG" \ + --default-model "$INPUT_MODEL" \ + --language "$INPUT_LANGUAGE" \ + --max-tokens "$INPUT_MAX_TOKENS" \ + --action-path "$ACTION_PATH" \ + --output "$RUNNER_TEMP/pi-multi-review-prompt.txt" + echo "prompt-file=$RUNNER_TEMP/pi-multi-review-prompt.txt" >> "$GITHUB_OUTPUT" + + - name: Run pi multi-review + uses: shaftoe/pi-coding-agent-action@v2 + with: + github_token: ${{ inputs.github-token }} + provider: ${{ inputs.provider }} + model: ${{ inputs.model }} + token: ${{ inputs.api-key }} + prompt: ${{ steps.build-prompt.outputs.prompt-file }} + extensions: "npm:pi-parallel-agents" + thinking_level: ${{ inputs.thinking-level }} + diff_max_lines: ${{ inputs.diff-max-lines }} + diff_ignore_patterns: ${{ inputs.diff-ignore-patterns }} + base_url: ${{ inputs.base-url }} diff --git a/pi-multi-review/reviewers/default.yml b/pi-multi-review/reviewers/default.yml new file mode 100644 index 0000000..34b9859 --- /dev/null +++ b/pi-multi-review/reviewers/default.yml @@ -0,0 +1,119 @@ +# Default reviewer personas for pi-multi-review +# Each reviewer runs as a parallel agent in the pi-parallel-agents team DAG. + +reviewers: + - name: quality + role: Code Quality Reviewer + prompt: | + You are a senior code quality reviewer. Review the following pull request diff. + + PR Title: {{title}} + PR Body: {{body}} + Changed Files: {{files}} + + Diff: + {{diff}} + + Focus on: + - Code readability and maintainability + - Naming conventions and consistency + - Error handling completeness + - DRY principle adherence + - Dead code or unused imports + - Proper use of language idioms + + Provide your findings as a structured list with severity levels (critical/warning/info). + Language: {{language}} + + - name: security + role: Security Reviewer + prompt: | + You are an application security expert. Review the following pull request diff for security vulnerabilities. + + PR Title: {{title}} + PR Body: {{body}} + Changed Files: {{files}} + + Diff: + {{diff}} + + Focus on: + - Input validation and sanitization + - SQL injection, XSS, CSRF risks + - Authentication and authorization issues + - Sensitive data exposure (secrets, tokens, PII) + - Insecure dependencies or APIs + - OWASP Top 10 categories + + Provide your findings as a structured list with severity levels (critical/warning/info). + Language: {{language}} + + - name: performance + role: Performance Reviewer + prompt: | + You are a performance optimization specialist. Review the following pull request diff for performance concerns. + + PR Title: {{title}} + PR Body: {{body}} + Changed Files: {{files}} + + Diff: + {{diff}} + + Focus on: + - Algorithmic complexity (O(n) implications) + - Unnecessary allocations or copies + - Database query efficiency (N+1 queries, missing indexes) + - Memory leaks or resource management + - Caching opportunities + - Async/concurrency issues + + Provide your findings as a structured list with severity levels (critical/warning/info). + Language: {{language}} + + - name: architecture + role: Architecture Reviewer + prompt: | + You are a software architect. Review the following pull request diff for architectural concerns. + + PR Title: {{title}} + PR Body: {{body}} + Changed Files: {{files}} + + Diff: + {{diff}} + + Focus on: + - Coupling and cohesion + - Separation of concerns + - Interface design and API contracts + - Design pattern applicability + - Scalability implications + - Backwards compatibility + - Dependency management + + Provide your findings as a structured list with severity levels (critical/warning/info). + Language: {{language}} + +synthesizer: + name: synthesizer + role: Review Synthesizer + prompt: | + You are a review synthesizer. Combine the following reviewer findings into a single coherent PR review comment. + + Reviewer Findings: + {task:quality-review} + {task:security-review} + {task:performance-review} + {task:architecture-review} + + Produce a structured review with these sections: + 1. Summary (2-3 sentences overall assessment) + 2. Critical Issues (items that MUST be fixed before merge, from any reviewer) + 3. Warnings (important but non-blocking concerns) + 4. Suggestions (nice-to-have improvements) + 5. Cross-Validated Findings (issues flagged by multiple reviewers - highlight these) + + Format as markdown suitable for a GitHub PR comment. + Start with a merge decision on the first line: one of "可合并", "有条件合并", or "不可合并". + Language: {{language}} diff --git a/pi-multi-review/scripts/build-prompt.py b/pi-multi-review/scripts/build-prompt.py new file mode 100644 index 0000000..19e6443 --- /dev/null +++ b/pi-multi-review/scripts/build-prompt.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Build the pi-parallel-agents team-mode DAG prompt for multi-review. + +Reads reviewer persona configs (custom or default), constructs a JSON team +DAG with all reviewers running in parallel and a synthesizer that depends on +them all, then writes the prompt to a file for the pi-coding-agent-action. +""" + +import argparse +import json +import sys +from pathlib import Path + +import yaml # PyYAML is available on GitHub Actions runners + + +def load_default_reviewers(action_path: str) -> dict: + """Load built-in default reviewer personas.""" + default_path = Path(action_path) / "reviewers" / "default.yml" + if not default_path.exists(): + print(f"error: default reviewers not found at {default_path}", file=sys.stderr) + sys.exit(1) + with open(default_path) as f: + return yaml.safe_load(f) + + +def load_custom_reviewers(config_str: str) -> dict: + """Parse inline YAML reviewer config.""" + return yaml.safe_load(config_str) + + +def build_team_dag(config: dict, default_model: str, language: str, max_tokens: int = 4096) -> dict: + """Build the pi-parallel-agents team-mode DAG from reviewer config.""" + reviewers = config.get("reviewers", []) + synthesizer_cfg = config.get("synthesizer", None) + + if not reviewers: + print("error: no reviewers defined", file=sys.stderr) + sys.exit(1) + + # Build team members + members = [] + for r in reviewers: + member = { + "role": r["name"], + "model": r.get("model", default_model), + "maxTokens": r.get("maxTokens", max_tokens), + } + members.append(member) + + # Add synthesizer member if defined + if synthesizer_cfg: + members.append({ + "role": synthesizer_cfg["name"], + "model": synthesizer_cfg.get("model", default_model), + "maxTokens": max_tokens, + }) + + # Build tasks: all reviewers run in parallel + tasks = [] + for r in reviewers: + task_prompt = r.get("prompt", "") + # Replace template variables (pi-coding-agent-action handles {{diff}} etc.) + task_prompt = task_prompt.replace("{{language}}", language) + tasks.append({ + "id": f"{r['name']}-review", + "assignee": r["name"], + "task": task_prompt, + }) + + # Add synthesizer task that depends on all reviewers + if synthesizer_cfg: + synth_prompt = synthesizer_cfg.get("prompt", "") + synth_prompt = synth_prompt.replace("{{language}}", language) + tasks.append({ + "id": "synthesize", + "assignee": synthesizer_cfg["name"], + "task": synth_prompt, + "depends": [t["id"] for t in tasks], + }) + + return { + "team": { + "objective": f"Multi-agent PR review with {len(reviewers)} parallel reviewers", + "members": members, + "tasks": tasks, + } + } + + +def main(): + parser = argparse.ArgumentParser(description="Build pi-multi-review prompt") + parser.add_argument("--reviewers-config", default="", help="Inline YAML reviewer config") + parser.add_argument("--default-model", default="claude-sonnet-4-6") + parser.add_argument("--language", default="Chinese") + parser.add_argument("--max-tokens", default="4096") + parser.add_argument("--action-path", required=True) + parser.add_argument("--output", required=True, help="Output file path for the prompt") + args = parser.parse_args() + + # Load config: custom overrides default + if args.reviewers_config.strip(): + config = load_custom_reviewers(args.reviewers_config) + else: + config = load_default_reviewers(args.action_path) + + # Build DAG + dag = build_team_dag(config, args.default_model, args.language, int(args.max_tokens)) + + # Write prompt as JSON string for pi-parallel-agents team mode + prompt = json.dumps(dag, ensure_ascii=False, indent=2) + + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(prompt) + print(f"Prompt written to {args.output}") + + +if __name__ == "__main__": + main()