diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml new file mode 100644 index 0000000..01bef10 --- /dev/null +++ b/.github/workflows/claude-mention.yml @@ -0,0 +1,52 @@ +name: Claude Mention + +# @claude-triggered review. Runs in the trusted base-repo context (so the token is +# available) — which is why it is gated to trusted commenters. Handles fork PRs +# WITHOUT executing their code: no npm scripts are granted here, so Claude reviews +# statically and cites the existing `validate-and-test` CI check (which already ran +# on the fork PR without secrets) as the source of truth for pass/fail. +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + mention: + # Fire only when: the comment is on a PR, mentions @claude, and the commenter is + # a trusted actor (prevents anonymous users burning subscription quota). + if: >- + ( + (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || + github.event_name == 'pull_request_review_comment' + ) && + contains(github.event.comment.body, '@claude') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Built-in Actions token for GitHub API; skips the OIDC exchange so no + # `id-token: write` and no Claude GitHub App install are required. + github_token: ${{ secrets.GITHUB_TOKEN }} + # No `prompt`: the @claude comment body is the instruction, so a maintainer + # can ask for a full review or a targeted question. + # Fork-safe tool set: read-only git + read utils for static inspection, and + # `gh pr comment` to post the reply. Deliberately NO `npm`/build execution — + # this path may target a fork, so it never runs the PR's code; it reviews + # statically and cites the existing `validate-and-test` CI check for pass/fail. + claude_args: | + --max-turns 30 + --allowedTools "Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git ls-tree:*),Bash(git cat-file:*),Bash(git rev-parse:*),Bash(git status:*),Bash(cat:*),Bash(ls:*),Bash(head:*),Bash(tail:*),Bash(grep:*),Bash(wc:*),Bash(find:*),Bash(jq:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checks:*),Bash(gh pr comment:*)" diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml new file mode 100644 index 0000000..e5b6e9e --- /dev/null +++ b/.github/workflows/claude-review.yml @@ -0,0 +1,87 @@ +name: Claude PR Review + +# Auto-review for pull requests opened from a branch inside THIS repo +# (maintainer/collaborators — trusted). Fork PRs fall through to a no-op via the +# job `if` guard and are instead handled by claude-mention.yml on an @claude +# comment. Uses the plain `pull_request` event (never pull_request_target). +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + review: + # Only run for non-draft PRs whose head branch lives in this repo (not a fork). + if: >- + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history so the reviewer can `git diff` against the base + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Use the built-in Actions token for GitHub API (post comments / read PR). + # Providing github_token makes the action SKIP the OIDC exchange, so no + # `id-token: write` permission and no Claude GitHub App install are needed. + # Trade-off: review comments post as github-actions[bot], not claude[bot]. + github_token: ${{ secrets.GITHUB_TOKEN }} + prompt: | + You are reviewing a pull request against the Droidnet Command Library — + a schema-driven engine + composer that builds serial commands for + Astromech droid boards. Most PRs edit a JSON board file under + libraries/boards/. Read CLAUDE.md and CONTRIBUTING.md for the full rules + before reviewing. + + Do all of the following, then post your review as exactly ONE PR + comment using: gh pr comment --repo --body "…". + Reference specific file:line locations inline in the summary text (you + do not have a GitHub inline-comment tool here). You are advisory only: + do NOT modify code, approve, or merge. + + 1. Verify the checks. Run: + npm ci + npm run validate + npm test + Report pass/fail with the actual output. If validate or a test fails, + point at the offending file and line and explain the fix. + + 2. Enforce the board-contribution rules (see CONTRIBUTING.md "What the + validator enforces"): + - Every command has at least one `examples` string. + - Every {placeholder} in a template has a matching param, and every + param maps to a placeholder (both directions). + - Every param.enum resolves to a defined enum. + - Command ids are unique across ALL board files. + - If any board changed, libraries/manifest.json `libraryVersion` is + bumped per semver (patch=fix, minor=add, major=rename/remove), and + releases.json is in sync if the released version changed. + - `confidence` is set honestly — unverified contributions use + `community`, not `high`. + + 3. HIGH SEVERITY — flag any safety-class mislabeling. A command that moves + the droid or affects power must NOT be marked safety:"cosmetic". Per + the project's Code of Conduct, mislabeling can cause real hardware to + move unexpectedly. Call this out prominently. + + 4. For changes to src/*.js: check correctness, keep the engine DOM-free + and the UI going through buildWCBValue/parseWCBValue, and preserve the + round-trip invariant buildWCBValue(parseWCBValue(v)) === v. + + Keep the summary concise and actionable. Reference specific file:line + locations for any line-specific issues. + claude_args: | + --max-turns 40 + --allowedTools "Bash(npm ci),Bash(npm run validate),Bash(npm test),Bash(npm run lint),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git ls-tree:*),Bash(git cat-file:*),Bash(git rev-parse:*),Bash(git status:*),Bash(cat:*),Bash(ls:*),Bash(head:*),Bash(tail:*),Bash(grep:*),Bash(wc:*),Bash(find:*),Bash(jq:*),Bash(gh pr comment:*)" diff --git a/docs/superpowers/plans/2026-07-06-automated-pr-review.md b/docs/superpowers/plans/2026-07-06-automated-pr-review.md new file mode 100644 index 0000000..2e057de --- /dev/null +++ b/docs/superpowers/plans/2026-07-06-automated-pr-review.md @@ -0,0 +1,399 @@ +# Automated Claude PR Review Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Automatically review every PR with Claude (flag issues, run checks, enforce board-contribution rules) and gate `main` so nothing merges without the maintainer's approval. + +**Architecture:** Two GitHub Actions workflows using `anthropics/claude-code-action@v1` authenticated with a Claude subscription OAuth token. An **auto** workflow reviews in-repo (trusted) PRs and runs `validate`+`test`; a **mention** workflow reviews fork PRs statically on a trusted-actor `@claude` comment. Branch protection on `main` requires approval + the existing CI check + conversation resolution, with admin bypass so the solo maintainer can still merge own PRs. + +**Tech Stack:** GitHub Actions (YAML), `anthropics/claude-code-action@v1`, `gh` CLI (REST API for branch protection), existing Node 20 CI (`npm run validate`, `npm test`). + +## Global Constraints + +- Never use `pull_request_target`; only the plain `pull_request` event. +- Claude is **review-only** on the auto path — no code edits, no approve, no merge. +- Fork PR code is **never executed** while the token is in the environment (mention path is static-review-only). +- Auth is the subscription token secret `CLAUDE_CODE_OAUTH_TOKEN` — never an API key, never a hardcoded token. +- GitHub API auth uses the built-in `github_token: ${{ secrets.GITHUB_TOKEN }}` input, which makes `claude-code-action@v1` skip the OIDC exchange — so **no `id-token: write` permission and no Claude GitHub App install** are required. Trade-off: review comments post as `github-actions[bot]` (not a branded `claude[bot]`) and don't sticky-update across pushes. (Discovered during execution: the default OIDC path failed without the App; `github_token` is the documented bypass — see the action's FAQ.) +- Restricted tool allowlist on the auto path: only `Bash(npm ci)`, `Bash(npm run validate)`, `Bash(npm test)`, `Bash(npm run lint)`. +- Existing CI job/check name is `validate-and-test` (from `.github/workflows/ci.yml`) — this exact string is the required status check. +- Default branch is `main`; repo is `travisccook/Droidnet-Command-Library` (public). +- Work happens on branch `feat/claude-pr-review` (already created; the spec is already committed there). + +## File Structure + +- Create: `.github/workflows/claude-review.yml` — auto-review for in-repo PRs (executes checks). +- Create: `.github/workflows/claude-mention.yml` — `@claude`-triggered static review, fork-safe. +- No file: branch protection on `main` is applied via the GitHub REST API (`gh api`). +- No file: the `CLAUDE_CODE_OAUTH_TOKEN` repo secret is set by the maintainer via `gh secret set`. + +--- + +### Task 1: Add the subscription token secret (maintainer prerequisite) + +**Files:** none (repo secret). + +**Interfaces:** +- Produces: repo secret `CLAUDE_CODE_OAUTH_TOKEN`, consumed by both workflows. + +This step involves pasting a secret and an interactive login, so the **maintainer runs it** (the agent cannot). In this session, run each with the `!` prefix so output lands in the conversation. + +- [ ] **Step 1: Generate a subscription OAuth token** + +Run (interactive — opens a browser / prompts): +```bash +claude setup-token +``` +Expected: prints a long-lived OAuth token string (starts with `sk-ant-oat...` or similar). Copy it. + +- [ ] **Step 2: Store it as a repo secret** + +Run (paste the token when prompted, or pipe it): +```bash +gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo travisccook/Droidnet-Command-Library +``` +Expected: `✓ Set Actions secret CLAUDE_CODE_OAUTH_TOKEN for travisccook/Droidnet-Command-Library` + +- [ ] **Step 3: Verify the secret exists** + +Run: +```bash +gh secret list --repo travisccook/Droidnet-Command-Library +``` +Expected: a row listing `CLAUDE_CODE_OAUTH_TOKEN`. (Value is not shown — that's correct.) + +--- + +### Task 2: Auto-review workflow (`claude-review.yml`) + +**Files:** +- Create: `.github/workflows/claude-review.yml` + +**Interfaces:** +- Consumes: secret `CLAUDE_CODE_OAUTH_TOKEN` (Task 1); CI check `validate-and-test` (existing). +- Produces: a workflow named "Claude PR Review" that fires on in-repo PRs and posts review comments. + +- [ ] **Step 1: Create the workflow file** + +Create `.github/workflows/claude-review.yml` with exactly: + +```yaml +name: Claude PR Review + +# Auto-review for pull requests opened from a branch inside THIS repo +# (maintainer/collaborators — trusted). Fork PRs fall through to a no-op via the +# job `if` guard and are instead handled by claude-mention.yml on an @claude +# comment. Uses the plain `pull_request` event (never pull_request_target). +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + review: + # Only run for non-draft PRs whose head branch lives in this repo (not a fork). + if: >- + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # github_token makes the action skip the GitHub OIDC exchange, so no + # `id-token: write` permission and no Claude GitHub App install are needed. + github_token: ${{ secrets.GITHUB_TOKEN }} + prompt: | + You are reviewing a pull request against the Droidnet Command Library — + a schema-driven engine + composer that builds serial commands for + Astromech droid boards. Most PRs edit a JSON board file under + libraries/boards/. Read CLAUDE.md and CONTRIBUTING.md for the full rules + before reviewing. + + Do all of the following, then post ONE summary comment plus inline + comments on specific lines. You are advisory only: do NOT modify code, + approve, or merge. + + 1. Verify the checks. Run: + npm ci + npm run validate + npm test + Report pass/fail with the actual output. If validate or a test fails, + point at the offending file and line and explain the fix. + + 2. Enforce the board-contribution rules (see CONTRIBUTING.md "What the + validator enforces"): + - Every command has at least one `examples` string. + - Every {placeholder} in a template has a matching param, and every + param maps to a placeholder (both directions). + - Every param.enum resolves to a defined enum. + - Command ids are unique across ALL board files. + - If any board changed, libraries/manifest.json `libraryVersion` is + bumped per semver (patch=fix, minor=add, major=rename/remove), and + releases.json is in sync if the released version changed. + - `confidence` is set honestly — unverified contributions use + `community`, not `high`. + + 3. HIGH SEVERITY — flag any safety-class mislabeling. A command that moves + the droid or affects power must NOT be marked safety:"cosmetic". Per + the project's Code of Conduct, mislabeling can cause real hardware to + move unexpectedly. Call this out prominently. + + 4. For changes to src/*.js: check correctness, keep the engine DOM-free + and the UI going through buildWCBValue/parseWCBValue, and preserve the + round-trip invariant buildWCBValue(parseWCBValue(v)) === v. + + Keep the summary concise and actionable. Prefer inline comments for + line-specific issues. + claude_args: | + --max-turns 20 + --allowedTools "Bash(npm ci),Bash(npm run validate),Bash(npm test),Bash(npm run lint)" +``` + +- [ ] **Step 2: Sanity-check YAML validity locally** + +Run (best-effort; `actionlint` if installed, otherwise a YAML parse): +```bash +if command -v actionlint >/dev/null; then actionlint .github/workflows/claude-review.yml; \ +elif command -v yq >/dev/null; then yq '.' .github/workflows/claude-review.yml >/dev/null && echo "YAML OK"; \ +else python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/claude-review.yml')); print('YAML OK')" 2>/dev/null || echo "no local yaml linter — GitHub validates on push"; fi +``` +Expected: `YAML OK` (or the "GitHub validates on push" fallback note — the live run in Task 5 is the real check). + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/claude-review.yml +git commit -m "ci: auto Claude review for in-repo pull requests" +``` + +--- + +### Task 3: Mention workflow (`claude-mention.yml`) + +**Files:** +- Create: `.github/workflows/claude-mention.yml` + +**Interfaces:** +- Consumes: secret `CLAUDE_CODE_OAUTH_TOKEN` (Task 1). +- Produces: a workflow named "Claude Mention" that runs on a trusted `@claude` PR comment and posts a static review. + +- [ ] **Step 1: Create the workflow file** + +Create `.github/workflows/claude-mention.yml` with exactly: + +```yaml +name: Claude Mention + +# @claude-triggered review. Runs in the trusted base-repo context (so the token is +# available) — which is why it is gated to trusted commenters. Handles fork PRs +# WITHOUT executing their code: no npm scripts are granted here, so Claude reviews +# statically and cites the existing `validate-and-test` CI check (which already ran +# on the fork PR without secrets) as the source of truth for pass/fail. +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + mention: + # Fire only when: the comment is on a PR, mentions @claude, and the commenter is + # a trusted actor (prevents anonymous users burning subscription quota). + if: >- + ( + (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || + github.event_name == 'pull_request_review_comment' + ) && + contains(github.event.comment.body, '@claude') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # github_token skips the GitHub OIDC exchange (no id-token / no App needed). + github_token: ${{ secrets.GITHUB_TOKEN }} + # No `prompt`: the @claude comment body is the instruction, so a maintainer + # can ask for a full review or a targeted question. + # No Bash(npm …) grant: this path may target a fork, so Claude reviews + # statically and refers to the existing CI check rather than executing the + # PR's code. Default tools = read/grep/glob + PR comments. + claude_args: | + --max-turns 15 +``` + +- [ ] **Step 2: Sanity-check YAML validity locally** + +```bash +if command -v actionlint >/dev/null; then actionlint .github/workflows/claude-mention.yml; \ +elif command -v yq >/dev/null; then yq '.' .github/workflows/claude-mention.yml >/dev/null && echo "YAML OK"; \ +else python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/claude-mention.yml')); print('YAML OK')" 2>/dev/null || echo "no local yaml linter — GitHub validates on push"; fi +``` +Expected: `YAML OK` (or the fallback note). + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/claude-mention.yml +git commit -m "ci: @claude mention review for fork PRs (trusted-actor gated, static)" +``` + +--- + +### Task 4: Push the branch and open the self-review PR + +**Files:** none. + +**Interfaces:** +- Consumes: Tasks 1–3 (secret + both workflows committed). +- Produces: an open PR from `feat/claude-pr-review` → `main` that the auto workflow reviews (the setup reviews itself). + +> Outward-facing: this creates a public PR. Confirm with the maintainer before running Step 2. + +- [ ] **Step 1: Push the branch** + +```bash +git push -u origin feat/claude-pr-review +``` +Expected: branch published; `gh` prints the compare URL. + +- [ ] **Step 2: Open the PR** + +```bash +gh pr create --repo travisccook/Droidnet-Command-Library --base main --head feat/claude-pr-review \ + --title "ci: automated Claude PR review + human merge gate" \ + --body "Adds two workflows (auto review for in-repo PRs; @claude static review for fork PRs) and a design/plan under docs/superpowers/. Branch protection applied separately. This PR is reviewed by the new setup itself." +``` +Expected: prints the new PR URL. + +- [ ] **Step 3: Confirm the auto workflow fired** + +Wait ~1–2 min, then: +```bash +gh run list --repo travisccook/Droidnet-Command-Library --workflow "Claude PR Review" --limit 3 +gh pr checks --repo travisccook/Droidnet-Command-Library feat/claude-pr-review +``` +Expected: a "Claude PR Review" run exists for this PR and is in progress or completed (not "startup_failure"). If it failed at auth, re-check Task 1's secret. + +- [ ] **Step 4: Confirm Claude posted a review** + +```bash +gh pr view --repo travisccook/Droidnet-Command-Library feat/claude-pr-review --comments | tail -40 +``` +Expected: a summary comment from Claude reporting the `validate`/`test` results and any findings. (This PR touches only workflow YAML + docs, so it should report the checks pass and note no board/engine changes.) + +--- + +### Task 5: Test the `@claude` mention path + +**Files:** none. + +**Interfaces:** +- Consumes: Task 3 workflow + open PR (Task 4). +- Produces: evidence the mention path runs for a trusted actor. + +- [ ] **Step 1: Trigger via comment (as the owner — a trusted actor)** + +```bash +gh pr comment --repo travisccook/Droidnet-Command-Library feat/claude-pr-review \ + --body "@claude please review this PR against the contribution rules" +``` +Expected: comment posted. + +- [ ] **Step 2: Confirm the mention workflow fired** + +Wait ~1 min, then: +```bash +gh run list --repo travisccook/Droidnet-Command-Library --workflow "Claude Mention" --limit 3 +``` +Expected: a "Claude Mention" run triggered by the comment. + +- [ ] **Step 3: Note the non-trusted case (observational)** + +No command — document expectation: a comment containing `@claude` from an account whose `author_association` is `CONTRIBUTOR`/`NONE` (i.e. an outside contributor) will **not** trigger the workflow because of the actor guard. Verify later if/when a real external comment occurs; do not fabricate a test account. + +--- + +### Task 6: Apply branch protection on `main` + +**Files:** none (GitHub REST API). + +**Interfaces:** +- Consumes: existing CI check `validate-and-test`. +- Produces: protected `main` — require PR + approval + CI green + conversation resolution; admin bypass ON. + +- [ ] **Step 1: Apply protection** + +```bash +gh api --method PUT repos/travisccook/Droidnet-Command-Library/branches/main/protection --input - <<'JSON' +{ + "required_status_checks": { + "strict": true, + "contexts": ["validate-and-test"] + }, + "enforce_admins": false, + "required_pull_request_reviews": { + "required_approving_review_count": 1, + "dismiss_stale_reviews": true, + "require_last_push_approval": false + }, + "required_conversation_resolution": true, + "restrictions": null +} +JSON +``` +Expected: JSON response describing the protection settings (HTTP 200). `enforce_admins.enabled: false` means you can still merge your own PRs. + +- [ ] **Step 2: Verify protection is in effect** + +```bash +gh api repos/travisccook/Droidnet-Command-Library/branches/main/protection \ + --jq '{approvals: .required_pull_request_reviews.required_approving_review_count, checks: .required_status_checks.contexts, conversations: .required_conversation_resolution.enabled, admins_enforced: .enforce_admins.enabled}' +``` +Expected: `{"approvals":1,"checks":["validate-and-test"],"conversations":true,"admins_enforced":false}` + +- [ ] **Step 3: Confirm the gate on the open PR** + +```bash +gh pr view --repo travisccook/Droidnet-Command-Library feat/claude-pr-review --json mergeStateStatus,mergeable +``` +Expected: `mergeStateStatus` is `BLOCKED` (needs the CI check green + conversations resolved) until those clear — proving nothing merges without passing the gate. As admin you can then merge this PR once CI is green and threads are resolved. + +--- + +## Verification Summary + +- Task 1: `gh secret list` shows `CLAUDE_CODE_OAUTH_TOKEN`. +- Tasks 2–3: both YAML files valid; committed. +- Task 4: "Claude PR Review" run fires on the self-PR and posts a summary. +- Task 5: "Claude Mention" run fires on a trusted `@claude` comment. +- Task 6: `main` protection returns the expected settings and the PR shows `BLOCKED` until the gate clears. + +## Rollback + +- Delete either workflow file and commit to disable that path. +- `gh api --method DELETE repos/travisccook/Droidnet-Command-Library/branches/main/protection` removes the merge gate. +- `gh secret delete CLAUDE_CODE_OAUTH_TOKEN` revokes the review's ability to authenticate. diff --git a/docs/superpowers/specs/2026-07-06-automated-pr-review-design.md b/docs/superpowers/specs/2026-07-06-automated-pr-review-design.md new file mode 100644 index 0000000..b8889ee --- /dev/null +++ b/docs/superpowers/specs/2026-07-06-automated-pr-review-design.md @@ -0,0 +1,257 @@ +# Automated Claude PR Review + Human Merge Gate — Design + +**Date:** 2026-07-06 +**Status:** Approved design, pending implementation +**Repo:** `travisccook/Droidnet-Command-Library` (public, community contributions) + +## Goal + +Every PR against this repo should be automatically reviewed by Claude — flagging +issues, verifying the checks pass, and enforcing the board-contribution rules — +after which nothing merges to `main` until **the maintainer approves**. Claude's +review is always **advisory**; a separate GitHub branch-protection gate is what +actually blocks a merge. + +## Current state (what already exists) + +- **CI** (`.github/workflows/ci.yml`) runs `npm run validate` + `npm test` on every + push and PR (Node 20). This already covers the "automatically test" requirement, + and — importantly — it runs on **fork PRs too** because `validate`/`test` need no + secrets. +- Repo is **public**, not a fork, default branch `main`. +- **No branch protection** on `main` yet. +- No PR template. +- Contribution rules are documented in `CONTRIBUTING.md` and `CLAUDE.md`. + +## Confirmed decisions + +| Decision | Choice | +| --- | --- | +| Review engine | GitHub Action `anthropics/claude-code-action@v1` (self-hosted workflow) | +| Trigger model | **Hybrid**: auto-review for in-repo PRs; `@claude` mention for fork PRs | +| Auth / billing | **Claude subscription** OAuth token (`CLAUDE_CODE_OAUTH_TOKEN`) | +| Merge gate | Branch protection: require approval + CI pass + conversation resolution; **admin bypass ON** (solo maintainer can merge own PRs) | +| Review depth | `npm run validate` + `npm test` + contribution-rule checks (no browser/UI automation) | +| Claude write access | **Review-only** on the auto path (no code edits, no approve/merge) | + +## Architecture + +Three pieces: two workflow files (the split enforces the fork-safety boundary), +one branch-protection config. + +``` +PR opened/updated ─┬─ from an in-repo branch (you/collaborators, TRUSTED) + │ └─▶ claude-review.yml (auto) ── runs validate+test, reviews, comments + └─ from a fork (UNTRUSTED) + └─▶ ci.yml runs validate+test (no secrets) → check result + maintainer comments "@claude review" + └─▶ claude-mention.yml (gated) ── static review, cites CI, comments +main branch protection ── require approval + CI green + conversations resolved ──▶ maintainer merges +``` + +### 1. `.github/workflows/claude-review.yml` — auto-review (trusted, in-repo PRs) + +- **Trigger:** `pull_request: [opened, synchronize, reopened]`. +- **Job guard:** run only when the PR head is in *this* repo and not a draft: + `if: github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.draft == false`. + Fork PRs fall through to a no-op (and wouldn't have the token anyway). +- **Permissions:** `contents: read`, `pull-requests: write`, `issues: write`. +- **Why it may execute code:** only collaborators can push in-repo branches, so + running `npm ci && npm run validate && npm test` here is safe and lets Claude + correlate a specific failure to a specific line for inline comments. +- **Tools (restricted):** + `--allowedTools "Bash(npm ci),Bash(npm run validate),Bash(npm test),Bash(npm run lint)"` + plus the default read/grep/glob and PR-comment tools. **No** general shell, **no** + code edits. +- **Auth:** `claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}`. + +Sketch: + +```yaml +name: Claude PR Review +on: + pull_request: + types: [opened, synchronize, reopened] +permissions: + contents: read + pull-requests: write + issues: write +jobs: + review: + if: >- + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + + claude_args: | + --max-turns 20 + --allowedTools "Bash(npm ci),Bash(npm run validate),Bash(npm test),Bash(npm run lint)" +``` + +### 2. `.github/workflows/claude-mention.yml` — `@claude` review (fork-safe) + +- **Trigger:** `issue_comment: [created]` and `pull_request_review_comment: [created]`. +- **Job guard (two conditions):** + 1. Comment is on a PR **and** contains `@claude`. + 2. Commenter is trusted: `author_association` ∈ `OWNER`, `MEMBER`, `COLLABORATOR`. + This stops a random passer-by from burning subscription quota by typing + `@claude`. +- **Runs in the base-repo trusted context** (that is how `issue_comment` works), so + the token is available — which is exactly why the actor guard matters. +- **Review-only, NO fork-code execution.** Because the target PR may be a fork, + Claude does **not** run `npm ci`/`npm run validate`/`npm test` here (executing a + fork's JS while the token is in the environment is an exfiltration risk). Instead + it **reads the diff statically and cites the existing `ci.yml` check** (which + already ran `validate`+`test` on the fork PR without secrets) as the source of + truth for pass/fail. Default `--allowedTools` (read/grep/glob + comments); no + `Bash(npm …)` execution grant. +- The comment text steers the request, so you can say `@claude review this against + the contribution rules` or ask a targeted question. + +Sketch: + +```yaml +name: Claude Mention +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] +permissions: + contents: read + pull-requests: write + issues: write +jobs: + mention: + if: >- + github.event.issue.pull_request != null && + contains(github.event.comment.body, '@claude') && + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # No prompt: the @claude comment is the instruction. + # No Bash(npm …) grant: static review only for fork safety. + claude_args: | + --max-turns 15 +``` + +> Note: exact `if` expression for detecting "comment is on a PR" will be finalized +> during implementation (`issue_comment` uses `github.event.issue.pull_request`; +> `pull_request_review_comment` is always on a PR). Both event shapes handled. + +### 3. The tailored review prompt (used by the auto path) + +Not a generic "review this PR." It instructs Claude to: + +1. **Verify the checks:** run `npm ci`, then `npm run validate` and `npm test` + (the suite auto-discovers every board and exercises each command's `examples`). + Report pass/fail with the actual output; if something fails, point at the + offending file/line. +2. **Enforce board-contribution rules** (from `CONTRIBUTING.md` / `CLAUDE.md`): + - ≥1 `examples` string per command. + - Every `{placeholder}` in a `template` ↔ a matching `param` (both directions). + - Every `param.enum` resolves to a defined enum. + - Command ids unique across **all** board files. + - `libraryVersion` bumped in `libraries/manifest.json` per semver, **and** + `releases.json` kept in sync when the released version changes. + - `confidence` set honestly — `community` (not `high`) for unverified + contributions. +3. **Flag safety-class mislabeling prominently** — e.g. a `movement`/`power` command + marked `cosmetic`. Per the Code of Conduct this can move real hardware; treat it + as high-severity. +4. **For `src/*.js` changes:** correctness; preserve the engine-stays-DOM-free / + UI-goes-through-`buildWCBValue`/`parseWCBValue` boundary; the round-trip + invariant `buildWCBValue(parseWCBValue(v)) === v`. +5. **Output:** a concise summary comment **plus inline comments** on specific lines. + Advisory only — do not modify code, approve, or merge. + +The prompt will reference `CLAUDE.md` and `CONTRIBUTING.md` so the rules load from +the repo rather than being duplicated verbatim (keeps the workflow in sync as the +docs evolve). + +### 4. Branch protection on `main` (the human merge gate) + +Applied via `gh api PUT /repos/{owner}/{repo}/branches/main/protection`: + +- **Require a pull request before merging** (no direct pushes to `main`). +- **Require status checks to pass:** the existing `validate-and-test` job + (strict / up-to-date-with-base). +- **Require conversation resolution** — every inline comment Claude leaves must be + resolved before the merge button unlocks. This is what ties Claude's findings to + the gate without hard-blocking on advisory output. +- **Require 1 approving review** — for external PRs this is your explicit ✅. +- **Admin bypass ON** (`enforce_admins: false`) — so you, as the sole maintainer, + can still merge your **own** PRs (GitHub forbids self-approval) once CI is green + and conversations are resolved. + +## Setup (one-time, done by the maintainer) + +1. Generate a subscription OAuth token locally: `claude setup-token`. +2. Store it as a repo secret: `gh secret set CLAUDE_CODE_OAUTH_TOKEN` (paste token). +3. (Implementation commits the two workflow files.) +4. Apply branch protection (implementation runs the `gh api` call, or hands the + maintainer the exact command to run). + +The Claude GitHub App install (`/install-github-app`) is **not required** — each +workflow passes `github_token: ${{ secrets.GITHUB_TOKEN }}` to the action, which +makes `claude-code-action@v1` **skip its GitHub OIDC exchange** (the OIDC path +requires both `id-token: write` *and* the Claude GitHub App installed). With the +built-in token, comments post via `github-actions[bot]` using the +`pull-requests`/`issues` write permissions declared above. Installing the App is an +optional later upgrade (branded `claude[bot]` identity, sticky-updating comments, +ability to trigger downstream workflows) — it would swap `github_token` for +`id-token: write`. + +## Security considerations (explicit) + +- **Fork PRs never execute untrusted code with the token present.** Auto path is + guarded to in-repo branches only; mention path is static-review-only. +- **Actor gating** on the mention path prevents quota abuse by anonymous commenters. +- **Restricted tool allowlist** — Claude may run only the four known npm scripts + (auto path) or nothing executable (mention path); no arbitrary shell, no edits. +- **Standard `pull_request` event**, never `pull_request_target` — avoids the + "pwn-request" secret-exposure class entirely. +- The action sanitizes prompt-injection vectors (hidden HTML/attrs) in PR content; + the actor gate is the primary defense on top of that. + +## Testing / verification plan + +1. **Workflow lint:** confirm both YAML files parse (e.g. `actionlint` if available, + or a trivial `yq`/GitHub validation on push to a branch). +2. **Auto path:** open a small in-repo test PR (e.g. a trivial board tweak on a + branch) → confirm `claude-review.yml` fires, runs validate+test, and posts a + summary + inline comment. +3. **Mention path:** on that PR (or a simulated fork), comment `@claude review` from + a trusted account → confirm `claude-mention.yml` fires and posts a static review; + confirm a comment from a non-trusted `author_association` does **not** trigger it. +4. **Merge gate:** confirm `main` now rejects a direct push; confirm the merge button + is blocked until CI is green + conversations resolved; confirm admin can merge an + own-PR without a second approver. +5. Tear down the test PR/branch. + +## Out of scope (YAGNI) + +- Managed "Code Review" (claude.ai admin feature) — requires Team/Enterprise. +- Auto-review of fork PRs without a maintainer trigger (would need + `pull_request_target` + heavier sandboxing). +- Claude auto-fixing / pushing commits — review-only by design; can be requested + ad hoc via `@claude` later if desired. +- Browser/UI automation of the composer/reference pages in CI. +- Issue triage automation (this design is PR review only). +```