# ----------------------------------------------------------------------------
# Example workflow distributed with the jira-sdlc plugin
# (https://github.com/kantorv/jira-sdlc-tools). It ships as an example to
# copy into your own repo's .github/workflows/ and adapt — it is not
# installed with the plugin.
#
#   Guide:         https://kantorv.github.io/jira-sdlc-tools/docs/applications/ci-review-pr-demo
#   All examples:  https://kantorv.github.io/jira-sdlc-tools/docs/applications
#   Source:        https://github.com/kantorv/jira-sdlc-tools/blob/main/docs/github-automations/example-workflows/demo-claude-reviewer.yml
# ----------------------------------------------------------------------------

# Demo — Claude-powered jira-task-reviewer on a PR (replaces the Kimi smoke test)
#
# What it demos: running the jira-task-reviewer skill headlessly in CI against a
# PR that already exists on the current branch. It is a Claude-powered rework of
# demo-kimi-openrouter-reviewer.yml, which did the same job with Kimi Code + a hand-written
# review prompt. Here the skill supplies its own instructions — the workflow's
# only job is to stand up the exact environment the skill's healthcheck demands,
# then invoke it.
#
# Trigger: a `/review` comment on a PR (issue_comment), restricted to OWNER or
# MEMBER commenters — issue_comment fires for any commenter on a public repo,
# so that author check is the security boundary. Deliberately not
# `pull_request: opened/reopened`: review is requested, not automatic, so it
# never fires on every push. This file was workflow_dispatch-only until JST-204;
# anything below that still says "dispatch branch" predates that change.
#
# The command may be bare or followed by a space or newline and free-form
# prose — `/review the auth changes only`. A parse step strips the command
# token and passes the remainder to the skill as its argument, which
# jira-task-reviewer documents as free-form notes about the run (focus areas,
# constraints, context). A bare `/review` gets the bare invocation this demo
# has always used. The guard is an exact match plus three `startsWith` forms,
# not the bare `startsWith(body, '/review')` it used to be — that one also
# fired on `/reviewer-anything`.
#
# demo-kimi-openrouter-reviewer.yml and reviewer.yml are left untouched — this is a new file.
#
# ── The shape (two jobs, mirroring demo-kimi-openrouter-reviewer.yml) ────────
#
#  1. check_pr_exists — resolves the commented-on PR (github.event.issue.number)
#     via `gh pr view`, validates its head branch, and skips the rest unless
#     that PR is OPEN on a feature/* or hotfix/* branch.
#  2. reviewer-claude — the new job. The old Kimi install/config/invoke steps are
#     replaced with: native claude CLI install (validator.yml's pattern), a
#     linked-worktree checkout of the PR head branch, and a headless
#     `claude -p "/jira-sdlc:jira-task-reviewer"` invocation.
#
# ── The one constraint that shapes the whole job (the "solved problem") ─────
#
#  The reviewer skill's healthcheck hard-stops unless it runs in a LINKED
#  WORKTREE (`.git` is a file) on a `feature/*` or `hotfix/*` branch. A bare
#  actions/checkout is a *main checkout* (`.git` is a directory), which FAILs
#  that check before any review happens. So this job, like
#  demo-claude-hotfix-flow.yml job 3, keeps actions/checkout as the main
#  checkout (on the PR's base branch) and rebuilds a linked worktree for the
#  PR's HEAD branch — the skill derives its Jira issue key from that branch
#  name and runs there. The skills themselves come from the marketplace
#  install, not from either checkout (caveat 3).
#
#  The head and base branches come from the PR itself (`gh pr view
#  --json headRefName,baseRefName`), NOT from the assigner job output the
#  hotfix-flow demo uses (this workflow has no upstream assigner job). Reading
#  the PR's real base works for both a sub-task PR (base = the parent branch)
#  and a single-step top-level PR (base = development), and sets
#  `parentbranch` so the skill's PR-base resolver gets the right answer on its
#  first source instead of falling back.
#
# ── Caveats worth knowing before you copy this into another repo ─────────────
#
#  1. **Do not export GH_TOKEN / GITHUB_TOKEN into the step that runs the
#     skill.** Every skill starts with statuscheck, which reads GITHUB_PAT_TOKEN
#     out of the env FILE (not the environment) and runs
#     `gh auth logout && gh auth login --with-token`; gh refuses that login
#     while either token variable is set — "first clear the value from the
#     environment", exit 1 — which FAILs the gh_auth row and aborts before any
#     review. The skill-running step below exports ONLY
#     CLAUDE_CODE_OAUTH_TOKEN; the one step that needs GH_TOKEN (resolve
#     head/base) sets it before the skill runs, never alongside it.
#  2. **Self-review.** The executor opens PRs with the same gh account this
#     reviewer logs in as, and GitHub blocks approving your own PR — the
#     reviewer skill already posts its verdict as a PR *comment* (with an
#     `APPROVED — …` / `CHANGES REQUESTED — …` prefix) for exactly this reason.
#  3. **The skills come from the marketplace, not from either checkout.** The
#     job installs the plugin with `claude plugin marketplace add` +
#     `claude plugin install`, which clones the plugin repo's *default* branch.
#     If a PR changes a skill file, this demo reviews that PR with the
#     *marketplace's* copy, not the PR head's — land skill changes on the
#     plugin repo's default branch if you want them exercised here.
#
# ── Secrets (repository secrets) ─────────────────────────────────────────────
#
#  Repository secrets (Settings → Secrets and variables → Actions) — no job
#  declares an environment. The env-file-writing step checks every one
#  up front and fails loud with `::error::` if any is empty/unset, rather than
#  letting the claude CLI die halfway through on a missing key.
#
#    CLAUDE_CODE_OAUTH_TOKEN  the claude CLI reads this from the environment
#                             — the only secret never written to the file
#    JIRA_ACCOUNT_URL         written to .jst/jira-sdlc-tools.local.env
#    JIRA_REVIEWER_EMAIL      } the reviewer's Jira identity
#    JIRA_REVIEWER_TOKEN      }
#    GITHUB_PAT_TOKEN         not a secret to create — always populated from
#                             the built-in GITHUB_TOKEN (which can post a
#                             review comment given the job permissions
#                             below). One limitation of the built-in token
#                             carries over unchanged: PRs opened with it
#                             don't trigger other workflows
#
#  Each is named exactly as the env-file key it becomes, so the bootstrap step
#  is one loop over a KEYS list (secret -> file via env, not sed) rather than a
#  hand-mapped list of printfs — the same pattern demo-kimi-openrouter-reviewer.yml uses, just
#  looped.

name: Reviewer Skill on PR (Claude demo)

on:
  issue_comment:
    types: [created]

# on:
#   workflow_dispatch:
#     inputs:
#       model:
#         description: 'Claude model to use'
#         required: false
#         default: 'sonnet'
#         type: choice
#         options:
#           - sonnet
#           - opus
#           - fable


env:
  # Pinned deliberately, same as validator.yml: a float would let an upstream
  # CLI change break this demo on an unrelated day. Workflow-level env can't
  # cross files, so validator.yml hardcodes the same pin — bump both together.
  CLAUDE_CLI_VERSION: 2.1.263
  DEFAULT_REVIEWER_MODEL: sonnet # TODO - select mechanics


jobs:
  # ── Gating job — cheap, secret-free, and runs before the reviewer job ─────
  # Resolves the commented-on PR and its issue key. Proceeds only when that PR
  # is OPEN on a feature/*|hotfix/* branch, so an unqualified comment is
  # rejected before a reviewer runner starts or a model token is spent.
  check_pr_exists:
    #
    # The command clause accepts the bare command or the command plus a
    # separator, so prose after it still fires the run. `fromJSON('"\n"')` is
    # how you spell a newline in a GitHub expression — there is no escape
    # sequence in its string literals; `\r` is checked too because comment
    # bodies come back from the API with CRLF line endings.
    if: |
      github.event.issue.pull_request &&
      (github.event.comment.body == '/review' ||
       startsWith(github.event.comment.body, '/review ') ||
       startsWith(github.event.comment.body, format('/review{0}', fromJSON('"\n"'))) ||
       startsWith(github.event.comment.body, format('/review{0}', fromJSON('"\r"')))) &&
      github.event.comment.author_association == 'OWNER'
    runs-on: ubuntu-latest
    name: Check PR exists
    timeout-minutes: 5
    permissions:
      contents: read
      pull-requests: read
    outputs:
      pr_exists: ${{ steps.check_pr.outputs.pr_exists }}
      pr_number: ${{ steps.check_pr.outputs.pr_number }}
      issue_key: ${{ steps.check_pr.outputs.issue_key }}
    steps:
      - name: Resolve the PR and its issue key
        # The triggering comment is on a PR (guarded above), so the PR number IS
        # github.event.issue.number, and the branch must be read from the PR.
        #
        # Do NOT use github.ref_name here: on an issue_comment event the ref is
        # always the repository's DEFAULT branch, never the PR head. This step
        # used to read it — correct while the trigger was workflow_dispatch,
        # where ref_name IS the dispatch branch — and when JST-204 switched the
        # trigger to issue_comment without changing it, every /review run died
        # here with "Branch name 'development' does not match required format".
        id: check_pr
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.issue.number }}
          REPO_NAME: ${{ github.repository }}
        run: |
          set -euo pipefail

          # One API call for both fields the gate needs.
          PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$REPO_NAME" --json headRefName,state)
          BRANCH_NAME=$(printf '%s' "$PR_JSON" | jq -r '.headRefName')
          PR_STATE=$(printf '%s' "$PR_JSON" | jq -r '.state')

          # A closed or merged PR is nothing to review — skip the reviewer job
          # rather than failing the run, since commenting on one isn't an error.
          if [ "$PR_STATE" != "OPEN" ]; then
            echo "PR #$PR_NUMBER is $PR_STATE — nothing to review."
            echo "pr_exists=0" >> "$GITHUB_OUTPUT"
            echo "pr_number=0" >> "$GITHUB_OUTPUT"
            echo "issue_key=" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          # Validate branch name format: must be feature/PROJ-NNN-slug or hotfix/PROJ-NNN-slug
          if [[ ! "$BRANCH_NAME" =~ ^(feature|hotfix)/[A-Z][A-Z0-9_]+-[0-9]+- ]]; then
            echo "::error::PR #$PR_NUMBER head branch '$BRANCH_NAME' does not match required format: feature/PROJ-NNN-slug or hotfix/PROJ-NNN-slug — the reviewer skill derives its issue key from the branch and hard-stops otherwise"
            echo "pr_exists=0" >> "$GITHUB_OUTPUT"
            echo "pr_number=0" >> "$GITHUB_OUTPUT"
            echo "issue_key=" >> "$GITHUB_OUTPUT"
            exit 1
          fi

          # Extract issue key from branch name (PROJ-NNN)
          ISSUE_KEY=$(printf '%s' "$BRANCH_NAME" | sed -nE 's#^(feature|hotfix)/([A-Z][A-Z0-9_]+-[0-9]+)-.*$#\2#p')
          if [ -z "$ISSUE_KEY" ]; then
            echo "::error::Could not extract issue key from branch name: $BRANCH_NAME"
            echo "pr_exists=0" >> "$GITHUB_OUTPUT"
            echo "pr_number=0" >> "$GITHUB_OUTPUT"
            echo "issue_key=" >> "$GITHUB_OUTPUT"
            exit 1
          fi

          echo "pr_exists=1" >> "$GITHUB_OUTPUT"
          echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
          echo "issue_key=$ISSUE_KEY" >> "$GITHUB_OUTPUT"
          echo "Found PR #$PR_NUMBER ($BRANCH_NAME) for issue $ISSUE_KEY"

  # ── Reviewer job — the Claude-powered rework of demo-kimi-openrouter-reviewer.yml's
  #    reviewer-smoke job ────────────────────────────────────────────────────
  reviewer-claude:
    name: Reviewer skill (Claude)
    needs: check_pr_exists
    if: needs.check_pr_exists.outputs.pr_exists == '1'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
      pull-requests: write # the reviewer posts its verdict as a PR comment

    steps:
      - name: Checkout
        # Full history + every remote ref: the linked worktree is built from a
        # fetched origin/<head>, and we stand on the PR's base branch, both of
        # which a shallow single-branch clone doesn't have.
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Resolve PR head, base, and issue key
        # The authoritative head/base come from the PR itself. Under an
        # issue_comment trigger github.ref_name is the repository's default
        # branch, not the PR head, so the ref is never a usable source here —
        # see the gating job. Re-deriving the key here lets us fail before
        # installing claude or spending tokens if the head isn't an issue branch.
        id: pr
        shell: bash
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ needs.check_pr_exists.outputs.pr_number }}
        run: |
          set -euo pipefail
          HEAD=$(gh pr view "$PR_NUMBER" --json headRefName --jq '.headRefName')
          BASE=$(gh pr view "$PR_NUMBER" --json baseRefName  --jq '.baseRefName')
          if [ -z "${HEAD:-}" ] || [ -z "${BASE:-}" ]; then
            echo "::error::could not resolve head/base refs for PR #$PR_NUMBER"
            exit 1
          fi
          # Generic <UPPER>-<digits> key (any project), so the worktree path
          # matches the worktree-<KEY> convention the skill expects without
          # needing PROJECT_KEY from the env file.
          KEY=$(printf '%s' "$HEAD" | sed -nE 's#^(feature|hotfix)/([A-Z][A-Z0-9_]*-[0-9]+)-.+$#\2#p')
          if [ -z "$KEY" ]; then
            echo "::error::PR #$PR_NUMBER head branch '$HEAD' is not a feature/* or hotfix/* issue branch — no <KEY>-<n> to derive, and the reviewer skill hard-stops otherwise. Dispatch this workflow from the PR's issue branch."
            exit 1
          fi
          echo "head=$HEAD"  >> "$GITHUB_OUTPUT"
          echo "base=$BASE"  >> "$GITHUB_OUTPUT"
          echo "key=$KEY"    >> "$GITHUB_OUTPUT"
          echo "### Reviewing \`$KEY\` — PR #$PR_NUMBER \`$HEAD\` → \`$BASE\`" >> "$GITHUB_STEP_SUMMARY"

      - name: Write .jst/jira-sdlc-tools.local.env (reviewer role only)
        shell: bash
        env:
          # Each secret is named exactly as the env-file key it becomes, so the
          # loop below is the whole secret->file mapping (env, not sed).
          KEYS: JIRA_ACCOUNT_URL JIRA_REVIEWER_EMAIL JIRA_REVIEWER_TOKEN GITHUB_PAT_TOKEN
          JIRA_ACCOUNT_URL: ${{ secrets.JIRA_ACCOUNT_URL }}
          JIRA_REVIEWER_EMAIL: ${{ secrets.JIRA_REVIEWER_EMAIL }}
          JIRA_REVIEWER_TOKEN: ${{ secrets.JIRA_REVIEWER_TOKEN }}
          GITHUB_PAT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # Checked here so one step catches every missing secret, but never
          # written to the file — the claude CLI reads it from the environment.
          CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
        run: |
          set -euo pipefail
          # Written in the *main checkout*, before the worktree exists: the
          # skill's own ensure_local_env.sh copies it into the worktree from
          # here, which is exactly the path that script exists to cover.
          missing=()
          for k in CLAUDE_CODE_OAUTH_TOKEN $KEYS; do
            [ -n "${!k:-}" ] || missing+=("$k")
          done
          if [ ${#missing[@]} -gt 0 ]; then
            echo "::error::missing or empty repo secret(s): ${missing[*]} — this demo cannot run without them. Add them under Settings → Secrets and variables → Actions."
            exit 1
          fi

          # Create empty and lock it down BEFORE the secrets go in.
          : > .jst/jira-sdlc-tools.local.env
          chmod 600 .jst/jira-sdlc-tools.local.env
          # printf with the value as an argument: secret values are data, never
          # re-expanded by the shell and never part of the script text.
          for k in $KEYS; do
            printf '%s=%s\n' "$k" "${!k}" >> .jst/jira-sdlc-tools.local.env
          done
          # WORKTREES_DIR is deliberately NOT a secret: it's a path on *this*
          # runner, and a value carried over from a developer's machine would
          # point somewhere that doesn't exist here.
          mkdir -p "$RUNNER_TEMP/worktrees"
          printf 'WORKTREES_DIR=%s\n' "$RUNNER_TEMP/worktrees" >> .jst/jira-sdlc-tools.local.env
          echo "Wrote $(grep -c . .jst/jira-sdlc-tools.local.env) config lines (values not echoed)."

      - name: Stand on the base branch
        # The workspace stays a *main checkout* (`.git` is a directory) on the
        # PR's base branch; the skill itself runs in the linked worktree built
        # below. Standing on the base (not the head) is what lets that worktree
        # be created at all — git refuses to check the same branch out twice.
        # The skills are not read from either checkout: they come from the
        # marketplace install — see caveat 3 in the header.
        shell: bash
        env:
          BASE: ${{ steps.pr.outputs.base }}
        run: |
          set -euo pipefail
          git switch "$BASE"
          # The reviewer is read-only — it never commits — so unlike
          # demo-claude-hotfix-flow.yml's committing jobs, no git user identity
          # is needed here. git switch's DWIM creates a local tracking branch
          # from origin/<base> when one doesn't exist locally yet.

      - name: Install claude CLI (native, no npm)
        # Same install as validator.yml and demo-claude-hotfix-flow.yml: the
        # installer SHA256-verifies a prebuilt binary against Anthropic's
        # manifest and runs its own `claude install`. Pinned deliberately — a
        # float would let an upstream CLI change break this demo on an unrelated
        # day. validator.yml and demo-claude-hotfix-flow.yml pin the same
        # version — bump all three together.
        shell: bash
        run: |
          set -euo pipefail
          curl -fsSL https://claude.ai/install.sh | bash -s -- "$CLAUDE_CLI_VERSION"
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: Install the jira-sdlc plugin from its marketplace
        shell: bash
        run: |
          set -euo pipefail
          # The skills come from the plugin's marketplace, not from this
          # checkout — which is what makes this workflow copy-pasteable into a
          # repo that does not contain the plugin. External consumers: swap the
          # URL below for your own fork or clone.
          #
          # `marketplace add` clones the source repo's DEFAULT branch, so what
          # runs here is whatever has landed there — not the code on the branch
          # under test. Land a skill change on the default branch before
          # expecting a demo run to exercise it.
          #
          # Spelled as an https:// URL rather than the `owner/repo` shorthand:
          # the shorthand tries SSH first and only falls back to HTTPS once that
          # fails, and a runner has no SSH key for github.com. Same marketplace
          # name (`jira-sdlc-tools`, from its manifest) either way.
          claude plugin marketplace add https://github.com/kantorv/jira-sdlc-tools.git
          claude plugin install jira-sdlc@jira-sdlc-tools

      - name: Rebuild the issue worktree (linked, from the PR head)
        # The prerequisite the bare checkout can't satisfy: a LINKED worktree on
        # the PR's head branch, where the skill runs and derives its issue key.
        # Without this the skill's statuscheck FAILs on the `worktree` row.
        id: worktree
        shell: bash
        env:
          HEAD: ${{ steps.pr.outputs.head }}
          BASE: ${{ steps.pr.outputs.base }}
          KEY: ${{ steps.pr.outputs.key }}
        run: |
          set -euo pipefail
          WT="$RUNNER_TEMP/worktrees/worktree-$KEY"
          git fetch origin "+refs/heads/$HEAD:refs/remotes/origin/$HEAD"
          git worktree add --track -B "$HEAD" "$WT" "origin/$HEAD"
          # parentbranch is machine-local git config, so it never survives into
          # CI. Re-set it to the PR's real base: first source the skill's §13
          # PR-base resolver reads, ahead of the assigner's durable "PR target
          # branch:" Jira comment. Correct for sub-task PRs (base = parent
          # branch) and single-step PRs (base = development) alike.
          git config "branch.$HEAD.parentbranch" "$BASE"
          echo "path=$WT" >> "$GITHUB_OUTPUT"

      - name: Parse the prose after the trigger command
        # Free-form direction may follow the command in the triggering comment
        # (`/review the auth changes only`). Strip the leading token,
        # trim, and expose the remainder as a step output; an empty remainder is
        # the normal case and leaves the run behaving exactly as a bare command
        # always did.
        #
        # This step parses, it never gates: the security boundary is the
        # job-level `if:` above and stays there, so this step only ever sees a
        # comment that already passed it. The body is attacker-supplied text —
        # it enters as an env var and leaves through `printf`, never as part of
        # the script.
        #
        # Identical in all five comment-triggered demos apart from COMMAND —
        # keep it that way, so a diff between two of them shows only what
        # genuinely differs.
        id: prose
        shell: bash
        env:
          COMMENT_BODY: ${{ github.event.comment.body }}
          COMMAND: /review
        run: |
          set -euo pipefail
          shopt -s extglob
          PROSE=${COMMENT_BODY:-}
          PROSE=${PROSE//$'\r'/}          # comment bodies arrive CRLF
          PROSE=${PROSE#"$COMMAND"}       # drop the leading command token
          PROSE=${PROSE##+([[:space:]])}  # trim both ends, newlines included
          PROSE=${PROSE%%+([[:space:]])}
          # Multi-line step output needs a delimiter, and a fixed one would let
          # prose containing that marker close the block early and forge further
          # outputs — so randomize it.
          DELIM="PROSE_EOF_$(openssl rand -hex 8)"
          {
            printf 'text<<%s\n' "$DELIM"
            printf '%s\n' "$PROSE"
            printf '%s\n' "$DELIM"
          } >> "$GITHUB_OUTPUT"
          if [ -n "$PROSE" ]; then
            printf 'Extra direction from the triggering comment: %s characters.\n' "${#PROSE}"
          else
            printf 'Bare command — no extra direction.\n'
          fi

      - name: Run jira-task-reviewer
        # No key: the reviewer derives the issue from the worktree's
        # branch. Run in the worktree; the skills come from the marketplace
        # install, so nothing points back into the checkout. Anything typed after
        # `/review` becomes the skill's argument — the free-form run notes it
        # documents — and a bare command keeps the bare invocation.
        #
        # --dangerously-skip-permissions: headless runs have no interactive
        # turn to approve tool calls. Safe-ish here and nowhere else: a
        # throwaway runner VM, non-root (the flag refuses to run as root).
        #
        # ONLY CLAUDE_CODE_OAUTH_TOKEN is exported here — see caveat 1 in the
        # header: exporting GH_TOKEN/GITHUB_TOKEN would block statuscheck's
        # gh login and abort the run before any review. The skill reads its
        # gh/Jira credentials from the env FILE (written above), not the
        # environment.
        timeout-minutes: 25
        shell: bash
        working-directory: ${{ steps.worktree.outputs.path }}
        env:
          CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          # Comment prose is user content: env var in, printf out, never
          # interpolated into the script.
          COMMENT_PROSE: ${{ steps.prose.outputs.text }}
        run: |
          set -euo pipefail
          # No prose → the exact bare invocation this demo has always used.
          PROMPT='/jira-sdlc:jira-task-reviewer'
          if [ -n "${COMMENT_PROSE:-}" ]; then
            PROMPT=$(printf -- '%s %s' "$PROMPT" "$COMMENT_PROSE")
          fi
          claude \
            --dangerously-skip-permissions \
            --model "$DEFAULT_REVIEWER_MODEL" \
            -p "$PROMPT" \
            2>&1 | tee "$RUNNER_TEMP/reviewer.log"

      - name: Comment the reviewer run metrics on the PR
        # This workflow posts no transcript comment of its own — the reviewer
        # skill owns the verdict on the PR — so this is the only comment this
        # job writes, and it carries what the run *cost* rather than what it
        # said. It goes on the PR (`gh pr comment`, the number resolved by
        # check_pr_exists) because that is where the review lives; the job
        # already declares `pull-requests: write`.
        #
        # `if: always()` — a failed run is exactly when the numbers are worth
        # most — plus `continue-on-error: true` following the GIF step's
        # precedent: a missing transcript or an absent jq must never redden a
        # green run. GH_TOKEN stays on the gh command in a step that runs AFTER
        # the skill; exporting it earlier would block statuscheck's gh login
        # and abort the review (caveat 1).
        if: always()
        continue-on-error: true
        shell: bash
        env:
          GITHUB_PAT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ needs.check_pr_exists.outputs.pr_number }}
          ROLE: reviewer
        run: |
          set -uo pipefail
          # The script exits 0 on every degrade path and never embeds the
          # transcript, so the body needs no truncation guard — a metrics table
          # is nowhere near gh's 65536-character limit.
          BODY="$RUNNER_TEMP/$ROLE-metrics.md"
          # The workspace stands on the PR's BASE branch, which on an older PR
          # can predate this script — hence the presence guard: a missing
          # script degrades to a line, like every other path here.
          SCRIPT="$GITHUB_WORKSPACE/.github/scripts/conversation_metrics.sh"
          {
            printf '### reviewer — conversation metrics\n\n'
            [ -f "$SCRIPT" ] && bash "$SCRIPT" \
              || printf '_%s is not on this checkout — nothing to measure._\n' "${SCRIPT##*/}"
          } > "$BODY"
          GH_TOKEN="$GITHUB_PAT_TOKEN" gh pr comment "$PR_NUMBER" --body-file "$BODY"

      - name: Upload reviewer log
        # The skill's full transcript — its verdict is echoed here as it posts
        # to GitHub and Jira, so a human inspecting the run sees the review.
        # The reviewer skill itself owns the report.md / Jira comment content;
        # this log is the reliable, path-independent artifact of what happened.
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: reviewer-log
          path: ${{ runner.temp }}/reviewer.log
          if-no-files-found: warn
          retention-days: 7



      - name: Render the reviewer transcript to a GIF
        if: always()
        continue-on-error: true
        timeout-minutes: 5
        shell: bash
        env:
          ROLE: reviewer
        run: |
          set -uo pipefail
          # A debug nicety, and it must never redden a green run — hence
          # `continue-on-error` + a step timeout, and deliberately NO `set -e`,
          # so each failure path below prints its own reason and the step ends
          # legibly instead of dying on an opaque non-zero. `if: always()` is
          # the point rather than a precaution: a *failed* skill run is exactly
          # the case a terminal replay beats 50k characters of piped stdout.
          # uv and uvx land in $HOME/.local/bin, which is already on PATH on
          # ubuntu-latest — prepended explicitly rather than assumed.
          export PATH="$HOME/.local/bin:$PATH"
          if ! pip install uv >/dev/null 2>&1; then
            echo "::warning::$ROLE GIF skipped — pip install uv failed"; exit 1
          fi
          if ! uv tool install agent-log-gif >/dev/null 2>&1; then
            echo "::warning::$ROLE GIF skipped — uv tool install agent-log-gif failed"; exit 1
          fi
          OUT="$RUNNER_TEMP/$ROLE-gif"
          # RENDER_LOG sits outside OUT: the artifact carries GIFs, not logs.
          RENDER_LOG="$RUNNER_TEMP/$ROLE-render.log"
          mkdir -p "$OUT"
          # Glob rather than computing the project slug: the slug differs per
          # job (job 1 runs claude from the workspace, jobs 2/3 from the
          # worktree under RUNNER_TEMP), and each job gets a fresh VM, so there
          # is normally exactly one file. Both other cases are handled rather
          # than assumed away.
          shopt -s nullglob
          FILES=("$HOME"/.claude/projects/*/*.jsonl)
          if [ ${#FILES[@]} -eq 0 ]; then
            echo "::notice::no Claude session JSONL found for $ROLE — nothing to render"
            exit 0
          fi
          rc=0
          for f in "${FILES[@]}"; do
            gif="$OUT/$(basename "$f" .jsonl).gif"
            # Renderer output goes to a file: its progress display would
            # otherwise be noise in the Actions log. Only the name, size and
            # final summary line are echoed.
            if uvx agent-log-gif json "$f" -o "$gif" \
                 --chrome linux --color-scheme Nord --show tools --turns 50 \
                 > "$RENDER_LOG" 2>&1; then
              echo "$(basename "$gif") — $(du -h "$gif" | cut -f1) — $(tail -1 "$RENDER_LOG")"
            else
              echo "::warning::$ROLE GIF render failed for $(basename "$f")"
              tail -5 "$RENDER_LOG"
              rc=1
            fi
          done
          exit $rc

      - name: Upload reviewer transcript GIF
        if: always()
        continue-on-error: true
        uses: actions/upload-artifact@v7
        with:
          name: reviewer-transcript-gif
          path: ${{ runner.temp }}/reviewer-gif/*.gif
          if-no-files-found: warn
          retention-days: 7
