diff --git a/.github/workflows/claude-issue-to-pr.yml b/.github/workflows/claude-issue-to-pr.yml new file mode 100644 index 0000000..a710a46 --- /dev/null +++ b/.github/workflows/claude-issue-to-pr.yml @@ -0,0 +1,128 @@ +name: Claude Issue to PR + +# Labeling an issue with `claude-fix:` kicks Claude off to +# investigate, make a focused change on a new branch, and open a PR +# linking back to the issue. The label's suffix scopes the ask (typo +# vs docs vs deps vs bug). +# +# Gated to HarperFast org members/collaborators: even though GitHub's +# permission model already restricts who can apply labels, we add an +# explicit author_association check on the issue author so mislabeling +# by an outsider can't trigger work. The action also performs its own +# write-access check on the labeler as a fallback. + +on: + issues: + types: [labeled] + +concurrency: + group: claude-issue-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + work: + # Only trigger for `claude-fix:*` labels AND when the issue was + # opened by a HarperFast org member or collaborator. Labels added + # to issues opened by outside contributors are ignored to keep + # the trigger surface tight during calibration. + if: >- + startsWith(github.event.label.name, 'claude-fix:') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), + github.event.issue.author_association) + runs-on: ubuntu-latest + timeout-minutes: 25 + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '22' + cache: 'npm' + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - name: Install dependencies + run: npm ci + + - name: Claude (agent mode) + id: claude-agent + uses: anthropics/claude-code-action@c3d45e8e941e1b2ad7b278c57482d9c5bf1f35b3 # v1.0.99 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + show_full_output: true + claude_args: | + --model claude-sonnet-4-6 + --max-turns 40 + --allowedTools "Read,Write,Edit,Grep,Glob,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*),Bash(gh pr create:*),Bash(gh issue view:*),Bash(gh issue comment:*),Bash(git:*),Bash(npm install:*),Bash(npm ci:*),Bash(npm run:*),Bash(npm test:*),Bash(bun install:*),Bash(bun run:*),Bash(bun test:*),Bash(npx:*)" + prompt: | + You were invoked because issue #${{ github.event.issue.number }} + on ${{ github.repository }} was labeled + `${{ github.event.label.name }}`. + + ## The ask + + Title: ${{ github.event.issue.title }} + + Body: + ${{ github.event.issue.body }} + + Source: ${{ github.event.issue.html_url }} + + ## Label-scoped behavior + + The label suffix tells you how much latitude you have: + + - `claude-fix:typo` — a single-file typo, prose tweak, or + tiny doc fix. Should be one or two lines changed. + - `claude-fix:docs` — a documentation update. Code should + not be touched unless the doc is literally a code comment. + - `claude-fix:deps` — a dependency version bump. Update + `package.json` and regenerate the lockfile via + `npm install` + verify `npm ci` works. + - `claude-fix:bug` — a focused bug fix with at least one + test that fails before the fix and passes after. + + Any ask that requires judgment beyond the label's scope — + new public API, architecture changes, cross-cutting + refactors — is OUT of scope. In that case, comment on the + issue explaining what you see and do NOT open a PR. + + ## Conventions + + Read `CLAUDE.md` in the repo root. Its "Code Conventions" + and "Non-Obvious Gotchas" sections apply. Match the repo's + style. + + ## Process + + 1. Create a branch named `claude/fix-${{ github.event.issue.number }}` + (or append `-` if useful). + 2. Make the change scoped to the label. + 3. Run `npm run build && npm run lint && npm run format:check && npm test` + and `bun test`. Fix anything that fails. + 4. Commit with a descriptive message. + 5. Push the branch and open a PR via `gh pr create` with a + body that says `Closes #${{ github.event.issue.number }}`. + 6. Post a comment on the original issue linking to the PR. + + ## Must NOT + + - Do NOT push to `main` or any `release_*` / `v*.x` branch. + - Do NOT use REQUEST_CHANGES or APPROVE on any PR. + - Do NOT open a PR when the ask is out of scope — comment + and stop. + - Do NOT commit secrets, credentials, or large generated + artifacts (e.g. `node_modules/`, coverage output). diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml new file mode 100644 index 0000000..920e123 --- /dev/null +++ b/.github/workflows/claude-mention.yml @@ -0,0 +1,105 @@ +name: Claude Mention Handler + +# Responds to `@claude …` in PR comments and PR review (inline) comments. +# Claude enters the action's "agent mode": reads the commenter's request, +# uses the PR/issue as context, and can edit + commit + push to the PR +# branch. Gated to HarperFast org members/collaborators so external +# contributors can't trigger work against the repo. + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +concurrency: + # Queue explicit mentions per PR/issue — don't cancel. A user who + # mentions Claude twice in quick succession probably wants both + # honored (or the second after the first finishes), not the first + # killed mid-flight. + group: claude-mention-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + work: + # Belt-and-suspenders gate: + # 1. Comment must contain the trigger phrase. + # 2. Commenter must be HarperFast org OWNER / MEMBER or a repo + # COLLABORATOR (the action also performs its own write-access + # check on the actor as a fallback). + if: >- + contains(github.event.comment.body, '@claude') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), + github.event.comment.author_association) + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + # Write access is intentional here — mention mode is "do work", + # which means editing files, committing, and pushing (either to + # the PR's branch or a new claude/… branch for issue-originated + # asks). + contents: write + pull-requests: write + issues: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '22' + cache: 'npm' + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - name: Install dependencies + run: npm ci + + - name: Claude (agent mode) + id: claude-agent + uses: anthropics/claude-code-action@c3d45e8e941e1b2ad7b278c57482d9c5bf1f35b3 # v1.0.99 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + show_full_output: true + claude_args: | + --model claude-sonnet-4-6 + --max-turns 30 + --allowedTools "Read,Write,Edit,Grep,Glob,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*),Bash(gh pr checkout:*),Bash(gh pr create:*),Bash(gh issue view:*),Bash(gh issue comment:*),Bash(git:*),Bash(npm install:*),Bash(npm ci:*),Bash(npm run:*),Bash(npm test:*),Bash(bun install:*),Bash(bun run:*),Bash(bun test:*),Bash(npx:*)" + # In agent mode the action uses the triggering comment as the + # prompt. The text below layers supplemental context on top. + prompt: | + You were invoked via an `@claude` mention on ${{ github.repository }}. + + ## Conventions + + Read `CLAUDE.md` in the repo root first. Its "Code Conventions" + and "Non-Obvious Gotchas" sections apply to any code you + write. Match the repo's existing style rather than + introducing a new one. + + ## Before committing + + Run `npm run build && npm run lint && npm run format:check && npm test` + in the workspace. Also run `bun test` — Bun is installed in + the runner. Fix anything that fails before committing. + + ## Output + + - Scope your changes to exactly what the mention asked for. + Don't refactor unrelated code. + - Commit with a descriptive message referencing the + issue/PR. + - If the request is ambiguous or you have to make a + judgment call that changes public API or semantics, post + a comment on the PR/issue explaining your interpretation + and stop — do NOT push speculative changes. + - Do NOT use REQUEST_CHANGES or APPROVE on PRs. Post + comments or push commits only. diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 9f62073..d1631c9 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -72,6 +72,13 @@ jobs: at start, you can re-invoke if needed) - `gh pr comment` — post the final review comment + Do NOT write files during the review — not to `.claude-pr/`, + not to `/tmp/`, not anywhere. The `Write` and `Edit` tools + are not allowed. If you want to organize notes, keep them + in-memory and assemble the final PR comment; saving + intermediate drafts to disk wastes turns on permission + denials. + Read `CLAUDE.md` in the repo root first — it has the project overview, conventions, and (importantly) a "Non-Obvious Gotchas" section covering Resource API v2 patterns, GenericTrackedObject @@ -149,6 +156,10 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} PR_URL: ${{ github.event.pull_request.html_url }} REVIEW_STATUS: ${{ steps.claude-review.outcome }} + # Short repo name for log title/label (e.g. "oauth" in + # "HarperFast/oauth"). Interpolated so this workflow can be + # copied to other HarperFast repos without source edits. + REPO_SHORT: ${{ github.event.repository.name }} run: | set -uo pipefail @@ -199,18 +210,19 @@ jobs: # didn't complete cleanly (e.g., max_turns). Findings posted before # the cutoff are still logged but flagged as potentially incomplete. if [ "$REVIEW_STATUS" = "success" ]; then - TITLE="[oauth] PR #$PR_NUMBER: $COUNT_PART" + TITLE="[$REPO_SHORT] PR #$PR_NUMBER: $COUNT_PART" else - TITLE="[oauth] PR #$PR_NUMBER: $COUNT_PART (review $REVIEW_STATUS — may be incomplete)" + TITLE="[$REPO_SHORT] PR #$PR_NUMBER: $COUNT_PART (review $REVIEW_STATUS — may be incomplete)" fi - BODY=$(printf '**Source:** %s\n**Repo:** oauth\n**PR:** #%s\n**Model:** claude-sonnet-4-6\n**Phase:** baseline\n**Review job status:** %s\n**Date:** %s\n\n---\n\n%s\n' \ - "$PR_URL" "$PR_NUMBER" "$REVIEW_STATUS" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$CLAUDE_BODY") + BODY=$(printf '**Source:** %s\n**Repo:** %s\n**PR:** #%s\n**Model:** claude-sonnet-4-6\n**Phase:** baseline\n**Review job status:** %s\n**Date:** %s\n\n---\n\n%s\n' \ + "$PR_URL" "$REPO_SHORT" "$PR_NUMBER" "$REVIEW_STATUS" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$CLAUDE_BODY") PAYLOAD=$(jq -nc \ --arg title "$TITLE" \ + --arg repo_label "repo:$REPO_SHORT" \ --arg body "$BODY" \ - '{title: $title, body: $body, labels: ["repo:oauth", "verdict:pending", "phase:baseline"]}') + '{title: $title, body: $body, labels: [$repo_label, "verdict:pending", "phase:baseline"]}') HTTP=$(curl -sS -o /tmp/ai-log-resp.json -w '%{http_code}' -X POST \ -H "Authorization: Bearer $AI_REVIEW_LOG_TOKEN" \