From 2ef29fd26af64a71458eb5bc7c72be28b67ea9c2 Mon Sep 17 00:00:00 2001 From: Nathan Heskew Date: Wed, 6 May 2026 09:03:26 -0700 Subject: [PATCH] =?UTF-8?q?ci(claude):=20Day=202=20caller=20migration=20?= =?UTF-8?q?=E2=80=94=20mention=20+=20issue-to-PR=20+=20caller=20validator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates the remaining two inline Claude workflows to thin callers of the reusables in HarperFast/ai-review-prompts (post-#14, all three reusables now exist): - `claude-mention.yml` → caller of `_claude-mention.yml` - `claude-issue-to-pr.yml` → caller of `_claude-issue-to-pr.yml` - `claude-review.yml` → SHA pin bumped from `bac5e45` to `11872cb` for parity with the other two (no functional change to the reusable; just keeping the three in sync to avoid drift) Adds `validate-caller-workflows.yml` — thin caller of `_validate-caller-workflows.yml`. This is the structural validator that catches shadow jobs and mutable refs in `.github/workflows/claude-*.yml`. Make this job a required status check on `main`. Repo-specific bits previously inlined in the workflow prompts now flow through the reusables' input contract: - `setup-bun: true` (this is a plugin repo with `bun test`) - `repo-specific-conventions:` carries the OAuth-specific notes (`config.yaml` is not doc-only, dual-runtime tests) and the documentation-scope-boundary section that reminds the agent to link to docs.harperdb.io rather than re-explaining Harper mechanics - `pre-commit-validation:` carries oauth's bun-aware test commands (label-scoped for issue-to-PR, change-kind-scoped for mention) Adds `.node-version` containing `24` so the new reusables (which use `node-version-file: '.node-version'` instead of a hard-coded `node-version: '22'`) resolve correctly. 24 is what oauth's `pr-checks.yml` and `integration-tests.yml` test in matrix already; engines say `>=22`. Major-only pin (no exact version) per the no-extra-pins-to-maintain preference. Removes oauth's now-redundant local copies: - `.github/scripts/authorize-claude-workflow.sh` (lived in oauth while inline workflows still used it; reusables ship their own copy from `ai-review-prompts`) - `.github/scripts/parse-claude-mention.sh` (same) - `.github/scripts/validate-auth-gate-invariants.sh` (replaced by the new caller-validator workflow, which fully covers the caller pattern; no inline workflows remain to validate) - `.github/workflows/auth-gate-invariants.yml` (replaced by `validate-caller-workflows.yml`) Net: 8 files touched, 175 added vs 849 removed (mostly the prompt bodies and scripts now centralized in `ai-review-prompts`). Workflow-modifying-PR caveat: this PR will hit the same App-token- 401 gotcha that oauth #71 and harper #478 hit — the migration PR itself won't get reviewed by Claude (checked-out workflow content differs from main's). Subsequent PRs review normally. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/scripts/authorize-claude-workflow.sh | 97 ----- .github/scripts/parse-claude-mention.sh | 37 -- .../scripts/validate-auth-gate-invariants.sh | 138 ------- .github/workflows/auth-gate-invariants.yml | 36 -- .github/workflows/claude-issue-to-pr.yml | 316 +++------------ .github/workflows/claude-mention.yml | 360 ++++-------------- .github/workflows/claude-review.yml | 4 +- .../workflows/validate-caller-workflows.yml | 39 ++ .node-version | 1 + 9 files changed, 177 insertions(+), 851 deletions(-) delete mode 100644 .github/scripts/authorize-claude-workflow.sh delete mode 100644 .github/scripts/parse-claude-mention.sh delete mode 100644 .github/scripts/validate-auth-gate-invariants.sh delete mode 100644 .github/workflows/auth-gate-invariants.yml create mode 100644 .github/workflows/validate-caller-workflows.yml create mode 100644 .node-version diff --git a/.github/scripts/authorize-claude-workflow.sh b/.github/scripts/authorize-claude-workflow.sh deleted file mode 100644 index 5498182..0000000 --- a/.github/scripts/authorize-claude-workflow.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -# Decide whether the trigger (PR author, comment author, labeler) is -# authorized to spawn a Claude workflow on this repo. Driven by the -# `authorize` job in claude-review.yml / claude-mention.yml / -# claude-issue-to-pr.yml. -# -# Trust set: every `@HarperFast/` handle in this repo's -# `.github/CODEOWNERS`. Same set as the people we trust to review code, -# aligned by construction. Falls back to `@HarperFast/developers` if -# CODEOWNERS is missing, empty, unparseable, or contains no HarperFast -# handles. External-org handles in CODEOWNERS are deliberately ignored -# — only HarperFast members are admitted. -# -# Inputs: -# USERS_TO_CHECK — newline-separated logins; ALL must pass. -# Empty / whitespace-only entries are skipped. -# ADMIT_CLAUDE_BOT — "true" admits `claude[bot]` without a team -# check (used by claude-review for AI-authored -# PRs from the issue-to-PR pipeline). Anything -# else requires team membership for every user. -# DEFAULT_TOKEN — token for the CODEOWNERS read (typically -# $GITHUB_TOKEN; needs `contents: read`). -# ORG_TOKEN — token for `orgs/.../teams/.../memberships/...` -# (App-installation token with `Members: Read`, -# scoped to this `authorize` job only). -# GITHUB_REPOSITORY — owner/repo (auto-set by GitHub Actions). -# GITHUB_OUTPUT — output file path. -# -# Outputs (to $GITHUB_OUTPUT): -# authorized=true|false -set -uo pipefail - -# Resolve the trust set from CODEOWNERS. The default token reads the -# workflow repo's own .github/CODEOWNERS via the contents API. -# Anything missing / empty / unparseable / containing no HarperFast -# handles falls back to the default team. -CODEOWNERS=$(GH_TOKEN="$DEFAULT_TOKEN" gh api \ - "repos/${GITHUB_REPOSITORY}/contents/.github/CODEOWNERS" \ - --jq '.content' 2>/dev/null | base64 -d 2>/dev/null || true) -TEAMS=$(printf '%s' "$CODEOWNERS" | grep -oE '@HarperFast/[a-zA-Z0-9_-]+' | sort -u | sed 's|@HarperFast/||' || true) - -if [ -z "$TEAMS" ]; then - echo "::notice::No @HarperFast/ handles found in .github/CODEOWNERS (missing, empty, or only external orgs). Defaulting to developers." - TEAMS="developers" -fi - -# Fail closed if USERS_TO_CHECK is empty or whitespace-only. The -# main loop below skips empty entries with `[ -z "$user" ] && continue` -# and would otherwise fall through to `authorized=true` if there was -# nothing to check. An authorize job that forgot to set USERS_TO_CHECK -# (or a malicious change that removed it) must NOT silently admit -# every event — refuse here. -if [ -z "${USERS_TO_CHECK//[[:space:]]/}" ]; then - echo "::error::USERS_TO_CHECK is empty or whitespace-only — denying by default. The authorize job must explicitly pass at least one login (PR author, commenter, labeler, etc.)." - echo "authorized=false" >> "$GITHUB_OUTPUT" - exit 0 -fi - -echo "Trust set (HarperFast teams from CODEOWNERS):" -for t in $TEAMS; do echo " - @HarperFast/$t"; done - -# is_authorized -# Admits claude[bot] iff ADMIT_CLAUDE_BOT=true; otherwise tries each -# team in the trust set in order. Returns 0 on the first hit. -is_authorized() { - local user="$1" - - if [ "${ADMIT_CLAUDE_BOT:-false}" = "true" ] && [ "$user" = "claude[bot]" ]; then - echo " → admitted: claude[bot]" - return 0 - fi - - for team in $TEAMS; do - # /orgs/{org}/teams/{team_slug}/memberships/{username} - # returns 200 for active members, 404 otherwise. - if GH_TOKEN="$ORG_TOKEN" gh api "orgs/HarperFast/teams/${team}/memberships/${user}" --silent >/dev/null 2>&1; then - echo " → admitted via @HarperFast/${team} membership" - return 0 - fi - done - - echo " → not a member of any HarperFast team in the trust set" - return 1 -} - -while IFS= read -r raw_user; do - user="$(printf '%s' "$raw_user" | awk '{$1=$1;print}')" - [ -z "$user" ] && continue - echo "Checking: $user" - if ! is_authorized "$user"; then - echo "User '$user' not authorized. Skipping the gated job." - echo "authorized=false" >> "$GITHUB_OUTPUT" - exit 0 - fi -done <<< "${USERS_TO_CHECK:-}" - -echo "authorized=true" >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/parse-claude-mention.sh b/.github/scripts/parse-claude-mention.sh deleted file mode 100644 index 30149fa..0000000 --- a/.github/scripts/parse-claude-mention.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# Decide whether to proceed with an `@claude` mention and which model -# to use, based on the comment body. Driven by claude-mention.yml's -# "Parse mention" step. -# -# Rules (the precision gate; the job-level `if:` is a cheap -# pre-filter that only checks substring containment): -# 1. `@claude` must be the FIRST non-whitespace token (word- -# boundary after) — rules out `@claudette`, inline prose -# mentions ("saw @claude's fix"), and quoted replies -# (`> @claude ...`) where the reply is addressing a human. -# 2. Case-insensitive word-boundary `deep` anywhere in the body -# escalates to Opus. Sonnet is the default. -# -# Inputs: -# BODY — comment body (verbatim) -# GITHUB_OUTPUT — output file path -# -# Outputs (to $GITHUB_OUTPUT): -# proceed=true|false -# model=claude-opus-4-7|claude-sonnet-4-6 (only when proceed=true) -set -uo pipefail - -if ! printf '%s' "$BODY" | grep -Pqz '\A\s*@claude\b'; then - echo "Comment does not start with @claude; skipping." - echo "proceed=false" >> "$GITHUB_OUTPUT" - exit 0 -fi - -if printf '%s' "$BODY" | grep -Piq '\bdeep\b'; then - echo "model=claude-opus-4-7" >> "$GITHUB_OUTPUT" - echo "Selected claude-opus-4-7 (deep requested)" -else - echo "model=claude-sonnet-4-6" >> "$GITHUB_OUTPUT" - echo "Selected claude-sonnet-4-6 (default)" -fi -echo "proceed=true" >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/validate-auth-gate-invariants.sh b/.github/scripts/validate-auth-gate-invariants.sh deleted file mode 100644 index c97efc8..0000000 --- a/.github/scripts/validate-auth-gate-invariants.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# Validate that the AI workflow auth gate structure is preserved -# across all `claude-*.yml` workflows. STRUCTURAL lint, not a semantic -# test — catches the obvious attacks (delete the authorize job, drop -# the `needs:` dependency, broaden permissions, change the -# if-expression to a tautology). Subtle attacks (e.g., modifying the -# bash logic inside the auth check to admit everyone) are out of -# scope for this validator and are caught by CODEOWNERS review on -# `.github/` changes. -# -# Defense in depth: branch-protection on `main` should make this -# workflow's job a REQUIRED status check. -# -# Inputs (none — runs in the workflow checkout). Validates: -# .github/workflows/claude-*.yml -# -# Exit code: -# 0 all workflows pass -# 1 any check failed (errors emitted as ::error::) -set -uo pipefail - -fail() { - echo "::error::$1" - exit 1 -} - -# yq is pre-installed on ubuntu-latest runners. -command -v yq >/dev/null || fail "yq not available on runner" - -shopt -s nullglob -files=(.github/workflows/claude-*.yml) -if [ "${#files[@]}" -eq 0 ]; then - echo "No claude-*.yml workflows found; nothing to validate." - exit 0 -fi - -for f in "${files[@]}"; do - echo "" - echo "=== Validating $f ===" - - # 0. Caller-pattern handling. Workflows that delegate to the reusable - # in HarperFast/ai-review-prompts (`.github/workflows/_claude-*.yml`) - # have no inline authorize job — the reusable owns that. The reusable's - # structural invariants are validated by ai-review-prompts' own - # auth-gate-invariants.yml. Here we just enforce that the caller pins - # to a 40-char SHA, not a branch or tag (mutable refs are a supply-chain - # risk — a tag could be silently repointed to weaken the auth gate). - if ! yq -e '.jobs.authorize' "$f" >/dev/null 2>&1; then - echo " ↪ no inline authorize job; treating as caller-pattern workflow" - callers=$(yq -r '.jobs[].uses | select(. != null)' "$f" 2>/dev/null | grep '^HarperFast/' || true) - if [ -z "$callers" ]; then - fail "$f: no inline authorize job AND no HarperFast/ reusable invocation — workflow has nothing gating it" - fi - while IFS= read -r caller; do - [ -z "$caller" ] && continue - ref="${caller##*@}" - if ! [[ "$ref" =~ ^[0-9a-f]{40}$ ]]; then - fail "$f: caller invocation '$caller' must pin to a 40-char SHA (got ref '$ref')" - fi - echo " ✓ pinned: $caller" - done <<< "$callers" - echo " ✓ $f passed (caller-pattern)" - continue - fi - - # 1. The authorize job exists. (Already verified above; the rest of - # these checks apply only to inline-authorize workflows.) - - # 2. authorize.outputs.authorized is wired to some step output. - output_expr=$(yq -r '.jobs.authorize.outputs.authorized // ""' "$f") - [ -n "$output_expr" ] \ - || fail "$f: authorize job has no outputs.authorized" - echo "$output_expr" | grep -q 'steps\..*\.outputs\.authorized' \ - || fail "$f: authorize.outputs.authorized must come from a step output (got: $output_expr)" - - # 3. authorize uses actions/create-github-app-token (pinned to a SHA). - app_token_step=$(yq -r '.jobs.authorize.steps[] | select(.uses != null) | .uses' "$f" | grep '^actions/create-github-app-token@' || true) - [ -n "$app_token_step" ] \ - || fail "$f: authorize doesn't use actions/create-github-app-token" - echo "$app_token_step" | grep -qE '@[0-9a-f]{40}( |$)' \ - || fail "$f: actions/create-github-app-token must be pinned to a 40-char SHA (got: $app_token_step)" - - # 4. authorize.permissions doesn't grant any write-level scope. - write_perms=$(yq -r '.jobs.authorize.permissions | (.[] // "") | select(. == "write")' "$f" 2>/dev/null || true) - [ -z "$write_perms" ] \ - || fail "$f: authorize.permissions grants 'write' on at least one scope — auth job must be read-only" - - # 5. Required secrets are referenced (the auth check can't work without them). - grep -q 'HARPERFAST_AI_CLIENT_ID' "$f" \ - || fail "$f: HARPERFAST_AI_CLIENT_ID secret not referenced" - grep -q 'HARPERFAST_AI_APP_PRIVATE_KEY' "$f" \ - || fail "$f: HARPERFAST_AI_APP_PRIVATE_KEY secret not referenced" - - # 6. The authorize job sets USERS_TO_CHECK on at least one of its - # steps. The auth script (`authorize-claude-workflow.sh`) fails - # closed if USERS_TO_CHECK is empty, but the workflow still - # shouldn't ship without it — make the omission a structural - # error rather than a silent runtime denial. Defense in depth - # against a PR that drops the env var thinking the script will - # "do the right thing". - # - # NOTE: yq on ubuntu-latest is mikefarah/yq (Go), not jq. It does - # NOT support jq's `empty` keyword, and an earlier version of this - # check using `// empty` lexer-erred silently (`2>/dev/null` ate it) - # and produced a false fail on workflows that DID set the env var. - # `select(. != null)` is the idiomatic yq filter for "skip steps - # without this env var"; `head -1` collapses the per-step stream to - # a single value (or empty). - users_to_check=$(yq -r '.jobs.authorize.steps[].env.USERS_TO_CHECK | select(. != null)' "$f" 2>/dev/null | head -1) - [ -n "$users_to_check" ] \ - || fail "$f: authorize job has no step setting USERS_TO_CHECK env var — the auth script needs at least one login to check (PR author, commenter, labeler, etc.)" - - # 7. Every non-authorize job has `needs: authorize` and a strict - # if-expression of exactly: needs.authorize.outputs.authorized == 'true' - # (whitespace normalized). Stricter than substring match — - # rules out tautologies like `... || true`. - other_jobs=$(yq -r '.jobs | keys | .[]' "$f" | grep -v '^authorize$' || true) - [ -n "$other_jobs" ] \ - || fail "$f: no non-authorize job found — workflow has nothing gated" - - for j in $other_jobs; do - needs=$(yq -r ".jobs.${j}.needs // \"\"" "$f") - [ "$needs" = "authorize" ] \ - || fail "$f: job '$j' must have 'needs: authorize' (got: $needs)" - - if_expr=$(yq -r ".jobs.${j}.if // \"\"" "$f") - # Normalize whitespace and quotes for the comparison. - normalized=$(echo "$if_expr" | tr -s ' ' | tr -d "\n") - expected="needs.authorize.outputs.authorized == 'true'" - [ "$normalized" = "$expected" ] \ - || fail "$f: job '$j' if: must be exactly \"$expected\" — no compound expressions, no tautologies (got: $if_expr)" - done - - echo " ✓ $f passed" -done - -echo "" -echo "All claude-*.yml workflows pass auth gate invariants." diff --git a/.github/workflows/auth-gate-invariants.yml b/.github/workflows/auth-gate-invariants.yml deleted file mode 100644 index 9ba9920..0000000 --- a/.github/workflows/auth-gate-invariants.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Auth gate invariants - -# Validates that the AI workflow auth gate structure is preserved. -# Runs on any PR that touches a `claude-*.yml` workflow file or this -# validator itself. -# -# This is a STRUCTURAL lint, not a semantic test. It catches the -# obvious attacks (delete the authorize job, drop the `needs:` -# dependency, broaden permissions, change the if-expression to a -# tautology). Subtle attacks (e.g., modifying the bash logic inside -# the auth check to admit everyone) are caught by CODEOWNERS review, -# which requires both @HarperFast/developers and @HarperFast/devops -# approval on `.github/` changes. -# -# Defense in depth — make this workflow a REQUIRED status check on -# `main` via branch protection so PRs can't merge without it passing. - -on: - pull_request: - paths: - - '.github/workflows/claude-*.yml' - - '.github/workflows/auth-gate-invariants.yml' - - '.github/scripts/validate-auth-gate-invariants.sh' - -jobs: - validate: - runs-on: ubuntu-latest - timeout-minutes: 2 - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Validate auth gate structure - run: bash .github/scripts/validate-auth-gate-invariants.sh diff --git a/.github/workflows/claude-issue-to-pr.yml b/.github/workflows/claude-issue-to-pr.yml index a68ef3b..91ae9de 100644 --- a/.github/workflows/claude-issue-to-pr.yml +++ b/.github/workflows/claude-issue-to-pr.yml @@ -1,15 +1,16 @@ 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). +# Thin caller of the reusable in HarperFast/ai-review-prompts. The +# single `uses:` ref pin below controls everything that moves +# together — workflow logic, auth script, agent prompt, allowed- +# labels list. Bumping the pin is the entire upgrade motion. # -# 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. +# Pre-requisites (org-level secrets, configured once on HarperFast): +# - HARPERFAST_AI_CLIENT_ID +# - HARPERFAST_AI_APP_PRIVATE_KEY +# +# Plus the per-repo / inherited: +# - ANTHROPIC_API_KEY (required) on: issues: @@ -20,248 +21,57 @@ concurrency: cancel-in-progress: false jobs: - authorize: - # Single source of truth for "is this label trigger allowed to - # spawn Claude on this repo?". The `work` job below has ONE `if:` - # that depends on this — no step-level guards, no individual user - # list. - # - # Cheap pre-filter at job level: explicit whitelist of allowed - # labels (NOT `startsWith('claude-fix:')`, which would match typoed - # variants like `claude-fix:typos`). - # - # We check the LABELER (`github.actor`), not the issue author. The - # labeler must already have at least triage permission to apply a - # label; a maintainer labeling an external-author issue is a - # legitimate way to invoke the agent on community reports. - # - # Trust set: the @HarperFast teams listed in this repo's - # `.github/CODEOWNERS`. Defaults to `@HarperFast/developers` if the - # file is missing, empty, or has no HarperFast handles. External- - # org handles are deliberately ignored. - # - # Required (organization-level) secrets: - # - HARPERFAST_AI_CLIENT_ID (the App's Client ID, like Iv23li…) - # - HARPERFAST_AI_APP_PRIVATE_KEY (.pem file contents) - if: contains(fromJSON('["claude-fix:typo","claude-fix:docs","claude-fix:deps","claude-fix:bug"]'), github.event.label.name) - runs-on: ubuntu-latest - timeout-minutes: 1 - permissions: - contents: read # for the .github/CODEOWNERS fetch - outputs: - authorized: ${{ steps.check.outputs.authorized }} - steps: - - name: Mint org-read token - id: app-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 - with: - client-id: ${{ secrets.HARPERFAST_AI_CLIENT_ID }} - private-key: ${{ secrets.HARPERFAST_AI_APP_PRIVATE_KEY }} - owner: HarperFast - - - name: Checkout (for CODEOWNERS read) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: | - .github/CODEOWNERS - .github/scripts/authorize-claude-workflow.sh - sparse-checkout-cone-mode: false - - - name: Check labeler authorization - id: check - env: - DEFAULT_TOKEN: ${{ github.token }} - ORG_TOKEN: ${{ steps.app-token.outputs.token }} - ADMIT_CLAUDE_BOT: 'false' - USERS_TO_CHECK: ${{ github.actor }} - run: bash .github/scripts/authorize-claude-workflow.sh - work: - needs: authorize - if: needs.authorize.outputs.authorized == 'true' - runs-on: ubuntu-latest - timeout-minutes: 25 - permissions: - contents: write - pull-requests: write - issues: write - id-token: write - - steps: - - name: Checkout - # Default shallow fetch (depth 1). The agent can commit and push on - # a shallow clone; `git log` / `git blame` aren't reached for by - # the current prompt. Bump to a deeper fetch only if we see the - # agent blocked on history lookups. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Clone shared Harper skills - # Pinned to a SHA (not `main`) so agent behavior is reproducible - # across runs — updates to the skills repo require an explicit - # pin bump in this workflow. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: HarperFast/skills - ref: d2db99bb37a6dde868cbc5ac81ca4146be8956fb # 1.3.0 (2026-04-16) - path: .harper-skills - - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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@ef50f123a3a9be95b60040d042717517407c7256 # v1.0.110 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - show_full_output: true - claude_args: | - --model claude-sonnet-4-6 - --max-turns 72 - # `--allowedTools` is convenience pre-approval, not a - # security boundary — see claude-mention.yml for the full - # rationale. Tools NOT listed below will still execute in - # CI; the entries here document the expected surface. - # - # Real containment for this workflow (issue-to-PR mode - # does AUTHORING work — branch creation, file edits, - # commits, push): - # * Token scope: `contents: write`, `pull-requests: write`, - # `issues: write`, `id-token: write`. Repo-scoped only. - # * Branch protection on protected refs (`main` / - # `release_*` / `v*.x`) — prevents pushes to those - # refs even with broader git access. - # * Auth gate — only trusted labelers (HarperFast team - # members) trigger the workflow at all. - # * Allowed labels list (`claude-fix:typo` / `:docs` / - # `:deps` / `:bug`) — narrow trigger surface. - # * Runner ephemerality. - # - # Notable absences in spirit (`Bash(npx:*)` deliberately - # not listed; `Bash(npm install)` no-arg) are prompt-level - # signals, not enforcement — same caveat as in - # claude-mention.yml. - --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:*)" - 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 (verbatim, including any multi-line content): - - ``` - ${{ 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 the repo's agent context files first (commonly - `CLAUDE.md`, `AGENTS.md`, or similar at the repo root). Their - "Code Conventions" and "Non-Obvious Gotchas" sections apply. - Match the repo's style. - - For docs and deployment-related changes — especially doc - issues like env-var / config / production-setup guidance — - also consult the shared Harper skills at - `.harper-skills/harper-best-practices/rules/`. In particular, - `.harper-skills/harper-best-practices/rules/deploying-to-harper-fabric.md` - is authoritative for Harper's deployment model (Fabric). - Do NOT invent generic production patterns (systemd units, - raw Kubernetes, cloud secrets managers, arbitrary .env - recommendations) without first checking whether a - Harper-specific path exists in those rules. If the skills - don't cover the ask and the issue body doesn't specify, - comment on the issue asking for clarification rather than - guessing. - - ## Documentation scope boundary - - Harper maintains authoritative documentation at - https://docs.harperdb.io covering core, pro, and fabric. This - repo's docs should NOT re-explain Harper mechanics that the - Harper docs already cover — they drift out of sync when the - Harper docs update. - - When writing or revising docs here: - - - Document what's specific to THIS component — env var names, - config shape, setup flow, integration API. - - For anything not component-specific (deployment mechanics, - runtime env var handling, Fabric configuration, core Harper - behavior, SQL, replication), LINK to the Harper docs rather - than re-explaining. - - This is a general rule, not specific to any one area. - - ## 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. Validate, scaling to the kind of change you made. - - **`config.yaml` is NOT doc-only** — it holds the plugin - entry point (`pluginModule`) and runtime OAuth defaults. - Any change that touches `config.yaml` requires the full - validation path regardless of the `claude-fix:*` label. - - - `claude-fix:typo` / `claude-fix:docs` (doc-only - changes to `*.md`, `docs/**`, or `package.json` - keyword/description fields): run - `npm run format:check` and `npm run lint`. Skip - `npm run build` / `npm test` / `bun test` — they - are not affected and waste turns. - - `claude-fix:deps` / `claude-fix:bug` or any change - that touches code or `config.yaml`: run - `npm run build && npm run lint && npm run format:check && npm test` - and `bun test`. Fix anything that fails. - - When in doubt, err toward the fuller validation. - 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). + uses: HarperFast/ai-review-prompts/.github/workflows/_claude-issue-to-pr.yml@11872cb1cc2d0e90659659ade6d8ddbbdfbf1d05 # main 2026-05-06 (post #11/#12/#14) + with: + # Same SHA as the `uses:` ref above. See the comment in + # claude-mention.yml for why the duplication is unavoidable. + ai-review-prompts-ref: 11872cb1cc2d0e90659659ade6d8ddbbdfbf1d05 + # Plugin repo — bun is part of the test path. + setup-bun: true + repo-specific-conventions: | + ## OAuth-specific notes + + - `config.yaml` holds the plugin entry point (`pluginModule`) + and runtime OAuth defaults. NOT doc-only — any change + there is a code/config change. + - Tests run under both Node (`npm test`) and Bun + (`bun test`); both must pass. + + ## Documentation scope boundary + + Harper maintains authoritative docs at + https://docs.harperdb.io covering core, pro, and fabric. + This repo's docs should NOT re-explain Harper mechanics + those docs already cover — they drift out of sync when the + Harper docs update. + + When writing or revising docs here: + + - Document what's specific to THIS component — env var + names, config shape, setup flow, integration API. + - For anything not component-specific (deployment mechanics, + runtime env var handling, Fabric configuration, core + Harper behavior, SQL, replication), LINK to the Harper + docs rather than re-explaining. + pre-commit-validation: | + **`config.yaml` is NOT doc-only** — it holds the plugin + entry point (`pluginModule`) and runtime OAuth defaults. + Any change touching `config.yaml` requires the full + validation path regardless of the `claude-fix:*` label. + + - `claude-fix:typo` / `claude-fix:docs` (doc-only changes + to `*.md`, `docs/**`, or `package.json` + keyword/description fields): run `npm run format:check` + and `npm run lint`. Skip `npm run build` / `npm test` / + `bun test` — they are not affected and waste turns. + - `claude-fix:deps` / `claude-fix:bug` or any change that + touches code or `config.yaml`: run + `npm run build && npm run lint && npm run format:check && npm test` + and `bun test`. Fix anything that fails. + + When in doubt, err toward the fuller validation. + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + HARPERFAST_AI_CLIENT_ID: ${{ secrets.HARPERFAST_AI_CLIENT_ID }} + HARPERFAST_AI_APP_PRIVATE_KEY: ${{ secrets.HARPERFAST_AI_APP_PRIVATE_KEY }} diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml index 8c7754d..6685c88 100644 --- a/.github/workflows/claude-mention.yml +++ b/.github/workflows/claude-mention.yml @@ -1,10 +1,16 @@ 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. +# Thin caller of the reusable in HarperFast/ai-review-prompts. The +# single `uses:` ref pin below controls everything that moves +# together — workflow logic, parse + auth scripts, agent prompt. +# Bumping the pin is the entire upgrade motion. +# +# Pre-requisites (org-level secrets, configured once on HarperFast): +# - HARPERFAST_AI_CLIENT_ID (the App's Client ID, like Iv23li…) +# - HARPERFAST_AI_APP_PRIVATE_KEY (.pem file contents) +# +# Plus the per-repo / inherited: +# - ANTHROPIC_API_KEY (required) on: issue_comment: @@ -13,290 +19,68 @@ on: 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: - authorize: - # Single source of truth for "is this commenter allowed to trigger - # Claude on this repo?". The `work` job below has ONE `if:` that - # depends on this — no step-level guards, no individual user list. - # - # Cheap pre-filter at job level: comment must mention `@claude`. - # The first-non-whitespace-token precision check is in the work - # job's `Parse mention` step. - # - # Trust set: the @HarperFast teams listed in this repo's - # `.github/CODEOWNERS`. Defaults to `@HarperFast/developers` if the - # file is missing, empty, or has no HarperFast handles. External- - # org handles are deliberately ignored — we only admit HarperFast - # members. - # - # Required (organization-level) secrets: - # - HARPERFAST_AI_CLIENT_ID (the App's Client ID, like Iv23li…) - # - HARPERFAST_AI_APP_PRIVATE_KEY (.pem file contents) - if: contains(github.event.comment.body, '@claude') - runs-on: ubuntu-latest - timeout-minutes: 1 - permissions: - # contents: read for the .github/CODEOWNERS fetch via default token. - contents: read - outputs: - authorized: ${{ steps.check.outputs.authorized }} - steps: - - name: Mint org-read token - id: app-token - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 - with: - client-id: ${{ secrets.HARPERFAST_AI_CLIENT_ID }} - private-key: ${{ secrets.HARPERFAST_AI_APP_PRIVATE_KEY }} - owner: HarperFast - - - name: Checkout (for CODEOWNERS read) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: | - .github/CODEOWNERS - .github/scripts/authorize-claude-workflow.sh - sparse-checkout-cone-mode: false - - - name: Check commenter authorization - id: check - env: - DEFAULT_TOKEN: ${{ github.token }} - ORG_TOKEN: ${{ steps.app-token.outputs.token }} - ADMIT_CLAUDE_BOT: 'false' - USERS_TO_CHECK: ${{ github.event.comment.user.login }} - run: bash .github/scripts/authorize-claude-workflow.sh - - work: - needs: authorize - if: needs.authorize.outputs.authorized == 'true' - 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 - # Default shallow fetch (depth 1). The agent can commit and push on - # a shallow clone; `git log` / `git blame` aren't reached for by - # the current prompt. Bump to a deeper fetch only if we see the - # agent blocked on history lookups. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Parse mention - # Real precision gate (the job-level `if:` is a cheap pre-filter). - # Enforces: - # 1. `@claude` must be the FIRST non-whitespace token (word- - # boundary after) — rules out `@claudette`, inline prose - # mentions ("saw @claude's fix"), and quoted replies - # (`> @claude ...`) where the reply is addressing a human. - # 2. Case-insensitive word-boundary `deep` anywhere in the body - # → escalate to Opus. Sonnet is the default. - id: mention - env: - BODY: ${{ github.event.comment.body }} - run: bash .github/scripts/parse-claude-mention.sh - - - name: Clone shared Harper skills - # Pinned to a SHA (not `main`) so agent behavior is reproducible - # across runs — updates to the skills repo require an explicit - # pin bump in this workflow. - if: steps.mention.outputs.proceed == 'true' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: HarperFast/skills - ref: d2db99bb37a6dde868cbc5ac81ca4146be8956fb # 1.3.0 (2026-04-16) - path: .harper-skills - - - name: Setup Node.js - # Needed so the agent can run `npm ci` / `npm run