magi-cli 0.2.0

Blind multi-agent implementation competition: N agents implement, M judges rank blind, deliberate, vote privately, winner survives double review + E2E gate
Documentation
name: claude

# Interactive @claude responder — mention `@claude` in a PR / issue
# comment (or an issue body/title) and Claude Code picks the task
# up: answer questions, implement requested changes on the PR
# branch, triage the issue, etc. Companion to the automatic
# `claude-review.yml`; this one only fires on explicit mentions, so
# it needs no review-skip policy.
#
# `.tera` suffix: same dual purpose as the other workflow templates
# here — keeps GHA from auto-running the source inside pj-base, and
# opts the file into kata's Tera rendering for the
# `{{ vars.actions.* }}` action-version pins.
#
# Setup requirement on every consumer (one-time): same pair as
# claude-review.yml.tera — the Claude GitHub App
# (github.com/apps/claude) installed on the repo, plus the
# `CLAUDE_CODE_OAUTH_TOKEN` secret from `claude setup-token`
# (Claude Pro/Max subscription). Pay-as-you-go alternative: store
# `ANTHROPIC_API_KEY` and swap the input below.
#
# `when = "always"` (in template.toml): every consumer wants the
# same responder; fixes to the workflow flow automatically.
#
# No `pull_request_review` trigger, and the reason is a merge
# blocker masquerading as a responder that did not fire. Replying
# to an inline review comment — `/pulls/{n}/comments/{id}/replies`,
# which is exactly the AGENTS.md "reply to reviewers in each
# thread" convention — makes GitHub create a NEW review whose
# summary body is EMPTY and fires `pull_request_review` with
# `review.body == ""`. The `@claude` sits in the comment, not the
# summary, so the job's condition cannot see it; the job skips,
# and a SKIPPED check run named "claude" then blocks the merge
# under branch protection until an admin bypasses it. Observed on
# yukimemi/hakari#18. In-thread replies are already caught by
# `pull_request_review_comment` above, so dropping the review event
# loses nothing for that flow. The only loss is an `@claude` placed
# in a review *summary*, which is not a documented pattern.

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]

permissions:
  # contents/pull-requests/issues write: Claude may push fix
  # commits to the PR branch and reply on threads. id-token for
  # the action's OIDC exchange; actions read so it can inspect CI
  # results when asked about failures.
  contents: write
  pull-requests: write
  issues: write
  id-token: write
  actions: read

concurrency:
  # Serialise responder runs per PR / issue — parallel @claude
  # mentions on the same thread could race pushing commits to the
  # same branch. No cancel: let the earlier request finish.
  group: claude-respond-${{ github.event.issue.number || github.event.pull_request.number }}
  cancel-in-progress: false

jobs:
  claude:
    # Two conditions, and the first is new because the bottom of this
    # file now hands the run a shell.
    #
    # `issue_comment` and friends always run the workflow from the
    # default branch *with secrets*, and the mention used to be the only
    # gate — so on a public repo any stranger could start this job. That
    # was survivable while the action had no way to execute anything:
    # the worst case was a wasted run. It stops being survivable the
    # moment `Bash(cargo test:*)` is in the allow-list, because
    # compiling a branch runs its `build.rs`, its proc-macros and its
    # test bodies. Gating on the author's association keeps "can run
    # code here" and "can push code here" the same set of people.
    #
    # The association lives under a different key per event type, and an
    # absent key is the empty string — so `||` picks the first one that
    # is present. The commenter's comes first, which is the one that
    # matters on a comment event; the issue author's is the fallback for
    # `issues`.
    if: |
      contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'),
               github.event.comment.author_association ||
               github.event.issue.author_association) &&
      (
        (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
        (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
        (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
      )
    runs-on: ubuntu-latest
    # Same rationale as claude-review.yml.tera: the action has no
    # wall-clock timeout of its own; bound a hung run well below
    # GHA's 360-minute default. 45 rather than 30 to match the review
    # job, for the same reason it went there — a mention that asks
    # "does this actually hold?" may now compile from a cold cache.
    timeout-minutes: 45
    steps:
      # Same guard as claude-review.yml.tera: an empty credential
      # makes claude-code-action exit 0 without responding, so a
      # mention would silently go unanswered with a green check next
      # to it. Accepts either `CLAUDE_CODE_OAUTH_TOKEN` (subscription)
      # or `ANTHROPIC_API_KEY` (pay-as-you-go, where the consumer also
      # swaps the input below to `anthropic_api_key`).
      #
      # Read via env rather than inlining the expressions into the
      # script, so the values are never substituted into the shell
      # source.
      - name: Check responder credentials
        env:
          CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          if [ -z "${CLAUDE_CODE_OAUTH_TOKEN}" ] && [ -z "${ANTHROPIC_API_KEY}" ]; then
            echo "::error title=claude responder is not configured::Neither CLAUDE_CODE_OAUTH_TOKEN nor ANTHROPIC_API_KEY is set on ${{ github.repository }}. Subscription path: generate a token with 'claude setup-token' and store it with 'gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo ${{ github.repository }}'. Pay-as-you-go path: set ANTHROPIC_API_KEY instead and swap the action input to anthropic_api_key. Until then @claude mentions in this repo go unanswered."
            exit 1
          fi

      - uses: actions/checkout@v7.0.1
        with:
          fetch-depth: 1
          # persist-credentials stays ON (the default) on purpose: the
          # action's setupBranch runs `git fetch origin <branch>` BEFORE
          # it configures its own git auth, so with the credential
          # scrubbed the fetch dies with "could not read Username for
          # 'https://github.com'" on private repos — public repos only
          # survive because the anonymous fetch happens to succeed
          # (anthropics/claude-code-action#1236). The App token the
          # action exchanges covers API calls, not that first fetch.

      # No-op on non-pnpm consumers (Rust, Bun, or anything else) —
      # gated on pnpm-lock.yaml so this only fires when a pj-pnpm
      # layer is actually composed in. Corepack ships with the
      # runner's preinstalled Node.js and reads package.json's
      # `packageManager` field to materialize the exact pinned pnpm
      # version, so no extra action or version pin is needed here.
      # Deliberately not `pnpm/action-setup`: this layer (pj-base) is
      # language-agnostic and has no `vars.actions.pnpm_action_setup`
      # pin available — that's seeded only by the optional pj-pnpm
      # layer's vars.pnpm.toml, and referencing it unconditionally
      # here would break Rust-only consumers that never seed it.
      - if: hashFiles('pnpm-lock.yaml') != ''
        run: corepack enable

      - uses: anthropics/claude-code-action@v1.0.195
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          # The same list `claude-review.yml.tera` carries, and for the
          # same reason: asked "is this claim true?", the responder had
          # no way to find out. It read the diff and reasoned, which is
          # worth something and is not worth as much as running the
          # thing — a review that verified by tracing missed a test
          # whose assertion was satisfied by an unrelated line, and only
          # caught it on the repo where it could edit a file and look.
          #
          # Deliberately no `git`, no `gh pr edit`, no writes: answering
          # a mention must not be able to change the branch it is
          # answering about. Compiling still executes `build.rs` and
          # test code, so this list bounds intent rather than
          # capability — which is why the job now checks who is asking.
          claude_args: |
            --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(cargo make:*),Bash(cargo test:*),Bash(cargo clippy:*),Bash(cargo check:*),Bash(cargo fmt:*),Bash(bun install:*),Bash(bun test:*),Bash(bun run build:*),Bash(bun run test:*),Bash(bun run lint:*),Bash(bun run check:*),Bash(bun run typecheck:*),Bash(pnpm install:*),Bash(pnpm build:*),Bash(pnpm lint:*),Bash(pnpm test:*)"