Files
ccs/.github/workflows/ai-review.yml
T
Tam Nhu Tran 97f07c2b12 refactor(ai-review): matrix strategy + orchestrator AI merge
- Replace 3 duplicate review jobs with 1 matrix job (-200 lines)
  Matrix entries: Security, Quality, CCS Compliance (run in parallel)
- Restore review-prompt.md as orchestrator merge prompt
  (dedup, severity rank, unified assessment, security+CCS tables)
- Aggregate job now runs claude-code-action with orchestrator prompt
  to produce polished unified review (not just stapled sections)
- Fallback to simple concat if orchestrator fails
- Dynamic prompt access via outputs[matrix.prompt_output] syntax
2026-03-28 21:02:57 -04:00

569 lines
23 KiB
YAML

# AI Code Review Workflow
# Uses anthropics/claude-code-action with GLM routing via GitHub App tokens
# Same-repo PRs stay on the self-hosted cliproxy runner; external PRs use ubuntu-latest
#
# Triggers:
# - Automatically when PR is opened, receives new commits, or is reopened
# - Manually via /review comment on PR
# - Manually via workflow_dispatch
#
# Pipeline:
# prepare → load-prompts → review (matrix: security, quality, ccs) → aggregate (orchestrator merge + publish)
#
# Note: Concurrency group cancels in-progress reviews when new commits arrive.
# This prevents wasting resources on outdated code reviews.
name: AI Code Review
on:
pull_request_target:
types: [opened, synchronize, reopened]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: string
# Smart concurrency: Prevents self-cancellation when non-command comments arrive
#
# Problem: When a bot or maintainer posts a normal PR note, GitHub fires an
# issue_comment event. A loose substring match on "/review" can accidentally
# treat text like "CI/review" as a manual command, which joins the PR
# concurrency group and cancels the in-progress review.
#
# Solution: Non-actionable triggers (bot comments, comments without /review) get a
# unique per-run group, while legitimate triggers share the PR-based group for proper
# cancellation of outdated reviews.
concurrency:
group: >-
ai-review-${{
github.event_name == 'issue_comment' && (
github.event.comment.user.type == 'Bot' ||
!startsWith(github.event.comment.body, '/review')
) && format('skip-{0}', github.run_id) ||
github.event.pull_request.number ||
github.event.issue.number ||
github.event.inputs.pr_number
}}
cancel-in-progress: true
jobs:
prepare:
name: Resolve review target
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
outputs:
pr_number: ${{ steps.context.outputs.pr_number }}
head_ref: ${{ steps.context.outputs.head_ref }}
head_sha: ${{ steps.context.outputs.head_sha }}
head_repo: ${{ steps.context.outputs.head_repo }}
author_login: ${{ steps.context.outputs.author_login }}
author_association: ${{ steps.context.outputs.author_association }}
contributor_source: ${{ steps.context.outputs.contributor_source }}
runs_on: ${{ steps.context.outputs.runs_on }}
# Conditions:
# - PR event: on opened, synchronize (new commits), or reopened
# - Comment event: only if it's a PR, starts with /review, and NOT from a bot
if: >
github.event_name == 'pull_request_target' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/review') &&
github.event.comment.user.type != 'Bot' &&
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.comment.author_association))
steps:
- name: Resolve PR metadata and runner
id: context
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }}
run: |
case "$EVENT_NAME" in
pull_request_target) PR_NUM="$EVENT_PR_NUMBER" ;;
issue_comment) PR_NUM="$EVENT_ISSUE_NUMBER" ;;
workflow_dispatch) PR_NUM="$INPUT_PR_NUMBER" ;;
*)
echo "Unsupported event: $EVENT_NAME" >&2
exit 1
;;
esac
PR_JSON="$(gh api "repos/$REPOSITORY/pulls/$PR_NUM")"
HEAD_REPO="$(jq -r '.head.repo.full_name' <<<"$PR_JSON")"
HEAD_REF="$(jq -r '.head.ref' <<<"$PR_JSON")"
HEAD_SHA="$(jq -r '.head.sha' <<<"$PR_JSON")"
AUTHOR_LOGIN="$(jq -r '.user.login' <<<"$PR_JSON")"
AUTHOR_ASSOCIATION="$(jq -r '.author_association' <<<"$PR_JSON")"
if [ "$HEAD_REPO" = "$REPOSITORY" ]; then
CONTRIBUTOR_SOURCE="internal"
RUNS_ON='["self-hosted","cliproxy"]'
else
CONTRIBUTOR_SOURCE="external"
RUNS_ON='["ubuntu-latest"]'
fi
{
echo "pr_number=$PR_NUM"
echo "head_ref=$HEAD_REF"
echo "head_sha=$HEAD_SHA"
echo "head_repo=$HEAD_REPO"
echo "author_login=$AUTHOR_LOGIN"
echo "author_association=$AUTHOR_ASSOCIATION"
echo "contributor_source=$CONTRIBUTOR_SOURCE"
echo "runs_on=$RUNS_ON"
} >> "$GITHUB_OUTPUT"
load-prompts:
name: Load review prompts
needs: prepare
if: needs.prepare.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
security_prompt: ${{ steps.prompts.outputs.security_prompt }}
quality_prompt: ${{ steps.prompts.outputs.quality_prompt }}
ccs_prompt: ${{ steps.prompts.outputs.ccs_prompt }}
base_ref: ${{ steps.prompts.outputs.base_ref }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Load prompts from base branch
id: prompts
env:
BASE_REF: ${{ github.base_ref || 'dev' }}
run: |
# Always load prompts from base branch to prevent PR-controlled prompt injection.
# External PRs could modify review prompts to suppress security findings.
git fetch origin "$BASE_REF" --depth=1 2>/dev/null || true
SECURITY_PROMPT=$(git show "origin/${BASE_REF}:.github/review-prompts/security.md" 2>/dev/null || echo "")
if [ -z "$SECURITY_PROMPT" ]; then
echo "::warning::security.md not found on base branch ${BASE_REF} — using inline fallback"
SECURITY_PROMPT="You are a security reviewer. Check the diff for injection vulnerabilities, auth bypasses, race conditions, secrets exposure, and supply chain risks. Report findings as: #### [HIGH|MEDIUM|LOW] [SECURITY] file:line"
fi
QUALITY_PROMPT=$(git show "origin/${BASE_REF}:.github/review-prompts/quality.md" 2>/dev/null || echo "")
if [ -z "$QUALITY_PROMPT" ]; then
echo "::warning::quality.md not found on base branch ${BASE_REF} — using inline fallback"
QUALITY_PROMPT="You are a code quality reviewer. Check for error handling gaps, false assumptions, performance issues, dead code, and test gaps. Report findings as: #### [HIGH|MEDIUM|LOW] [QUALITY] file:line"
fi
CCS_PROMPT=$(git show "origin/${BASE_REF}:.github/review-prompts/ccs-compliance.md" 2>/dev/null || echo "")
if [ -z "$CCS_PROMPT" ]; then
echo "::warning::ccs-compliance.md not found on base branch ${BASE_REF} — using inline fallback"
CCS_PROMPT="You are a CCS compliance reviewer. Check for: no emojis in CLI output, getCcsDir() usage, cross-platform parity, --help updates, string-only settings, conventional commits. Report findings as: #### [HIGH|MEDIUM|LOW] [CCS] file:line"
fi
SECURITY_DELIM="SECURITY_$(openssl rand -hex 16)"
echo "security_prompt<<${SECURITY_DELIM}" >> "$GITHUB_OUTPUT"
printf '%s\n' "$SECURITY_PROMPT" >> "$GITHUB_OUTPUT"
echo "${SECURITY_DELIM}" >> "$GITHUB_OUTPUT"
QUALITY_DELIM="QUALITY_$(openssl rand -hex 16)"
echo "quality_prompt<<${QUALITY_DELIM}" >> "$GITHUB_OUTPUT"
printf '%s\n' "$QUALITY_PROMPT" >> "$GITHUB_OUTPUT"
echo "${QUALITY_DELIM}" >> "$GITHUB_OUTPUT"
CCS_DELIM="CCS_$(openssl rand -hex 16)"
echo "ccs_prompt<<${CCS_DELIM}" >> "$GITHUB_OUTPUT"
printf '%s\n' "$CCS_PROMPT" >> "$GITHUB_OUTPUT"
echo "${CCS_DELIM}" >> "$GITHUB_OUTPUT"
echo "base_ref=$BASE_REF" >> "$GITHUB_OUTPUT"
review:
name: "${{ matrix.name }} Review"
needs: [prepare, load-prompts]
if: needs.prepare.result == 'success'
timeout-minutes: 10
runs-on: ${{ fromJSON(needs.prepare.outputs.runs_on) }}
strategy:
fail-fast: false
matrix:
include:
- name: Security
prompt_output: security_prompt
output_file: security_review.md
artifact_prefix: security
- name: Quality
prompt_output: quality_prompt
output_file: quality_review.md
artifact_prefix: quality
- name: CCS Compliance
prompt_output: ccs_prompt
output_file: ccs_review.md
artifact_prefix: ccs
permissions:
contents: read
pull-requests: read
issues: read
env:
ANTHROPIC_BASE_URL: https://api.z.ai/api/anthropic
REVIEW_MODEL: glm-5.1
ANTHROPIC_AUTH_TOKEN: ${{ secrets.GLM_API_KEY }}
ANTHROPIC_MODEL: glm-5.1
ANTHROPIC_DEFAULT_OPUS_MODEL: glm-5.1
ANTHROPIC_DEFAULT_SONNET_MODEL: glm-5.1
ANTHROPIC_DEFAULT_HAIKU_MODEL: GLM-4.7-FlashX
DISABLE_BUG_COMMAND: '1'
DISABLE_ERROR_REPORTING: '1'
DISABLE_TELEMETRY: '1'
CLAUDE_CODE_MAX_OUTPUT_TOKENS: '32000'
MAX_THINKING_TOKENS: '8000'
REVIEW_OUTPUT_FILE: ${{ matrix.output_file }}
steps:
- name: Prepare isolated Claude runtime
run: |
REVIEW_HOME="$RUNNER_TEMP/claude-home"
mkdir -p "$REVIEW_HOME" "$RUNNER_TEMP/xdg-config" "$RUNNER_TEMP/xdg-cache" "$RUNNER_TEMP/xdg-state"
{
echo "HOME=$REVIEW_HOME"
echo "XDG_CONFIG_HOME=$RUNNER_TEMP/xdg-config"
echo "XDG_CACHE_HOME=$RUNNER_TEMP/xdg-cache"
echo "XDG_STATE_HOME=$RUNNER_TEMP/xdg-state"
} >> "$GITHUB_ENV"
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout PR code
run: |
git fetch origin "refs/pull/${{ needs.prepare.outputs.pr_number }}/head"
git checkout --force FETCH_HEAD
- name: "Run ${{ matrix.name }} Review"
id: claude-review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.GLM_API_KEY }}
github_token: ${{ github.token }}
show_full_output: true
track_progress: false
prompt: |
think
You are a ${{ matrix.name }}-focused code reviewer for PR #${{ needs.prepare.outputs.pr_number }} in ${{ github.repository }}.
PR HEAD SHA: ${{ needs.prepare.outputs.head_sha }}
${{ needs.prepare.outputs.contributor_source == 'external' && 'EXTERNAL PR: Apply maximum scrutiny.' || '' }}
Follow the repository CLAUDE.md for project-specific guidelines.
${{ needs.load-prompts.outputs[matrix.prompt_output] }}
## IMPORTANT: Writing the Review
After completing your analysis, use the `Write` tool to write your findings to `${{ env.REVIEW_OUTPUT_FILE }}`.
Use `Write` tool directly — do NOT use `Edit`.
Do NOT post GitHub comments. Do NOT modify source code.
IMPORTANT: Do NOT use shell operators (|| &&) or heredoc (<<) in bash commands.
claude_args: |
--bare
--model ${{ env.REVIEW_MODEL }}
--permission-mode bypassPermissions
--max-turns 25
- name: Fallback extraction
if: always() && steps.claude-review.outcome != 'cancelled'
run: |
if [ -s "$REVIEW_OUTPUT_FILE" ]; then exit 0; fi
EXEC_LOG="$RUNNER_TEMP/claude-execution-output.json"
if [ ! -f "$EXEC_LOG" ]; then echo "${{ matrix.name }} review not available." > "$REVIEW_OUTPUT_FILE"; exit 0; fi
EXTRACTED=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // empty' "$EXEC_LOG" 2>/dev/null || true)
if [ -n "$EXTRACTED" ]; then
printf '%s\n' "$EXTRACTED" > "$REVIEW_OUTPUT_FILE"
else
echo "${{ matrix.name }} review produced no output." > "$REVIEW_OUTPUT_FILE"
fi
- name: Upload review output
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_prefix }}-review-${{ needs.prepare.outputs.pr_number }}-run${{ github.run_id }}
path: ${{ env.REVIEW_OUTPUT_FILE }}
retention-days: 3
if-no-files-found: warn
- name: Upload execution log
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_prefix }}-exec-log-pr${{ needs.prepare.outputs.pr_number }}-run${{ github.run_id }}
path: ${{ runner.temp }}/claude-execution-output.json
retention-days: 7
if-no-files-found: ignore
aggregate:
name: Merge & Publish Review
needs: [prepare, load-prompts, review]
if: always() && needs.prepare.result == 'success'
timeout-minutes: 10
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
env:
ANTHROPIC_BASE_URL: https://api.z.ai/api/anthropic
REVIEW_MODEL: glm-5.1
ANTHROPIC_AUTH_TOKEN: ${{ secrets.GLM_API_KEY }}
ANTHROPIC_MODEL: glm-5.1
ANTHROPIC_DEFAULT_OPUS_MODEL: glm-5.1
ANTHROPIC_DEFAULT_SONNET_MODEL: glm-5.1
ANTHROPIC_DEFAULT_HAIKU_MODEL: GLM-4.7-FlashX
DISABLE_BUG_COMMAND: '1'
DISABLE_ERROR_REPORTING: '1'
DISABLE_TELEMETRY: '1'
CLAUDE_CODE_MAX_OUTPUT_TOKENS: '64000'
MAX_THINKING_TOKENS: '16000'
REVIEW_OUTPUT_FILE: merged_review.md
REVIEW_COMMENT_FILE: .ccs-ai-review-comment.md
steps:
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CCS_REVIEWER_APP_ID }}
private-key: ${{ secrets.CCS_REVIEWER_PRIVATE_KEY }}
- name: Add eyes reaction to /review comment
if: github.event_name == 'issue_comment'
run: |
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
--method POST -f content=eyes
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Download all review artifacts
uses: actions/download-artifact@v4
with:
pattern: "*-review-${{ needs.prepare.outputs.pr_number }}-run${{ github.run_id }}"
merge-multiple: true
continue-on-error: true
- name: Prepare isolated Claude runtime
run: |
REVIEW_HOME="$RUNNER_TEMP/claude-home"
mkdir -p "$REVIEW_HOME" "$RUNNER_TEMP/xdg-config" "$RUNNER_TEMP/xdg-cache" "$RUNNER_TEMP/xdg-state"
rm -f "$REVIEW_OUTPUT_FILE" "$REVIEW_COMMENT_FILE"
{
echo "HOME=$REVIEW_HOME"
echo "XDG_CONFIG_HOME=$RUNNER_TEMP/xdg-config"
echo "XDG_CACHE_HOME=$RUNNER_TEMP/xdg-cache"
echo "XDG_STATE_HOME=$RUNNER_TEMP/xdg-state"
} >> "$GITHUB_ENV"
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout PR code
run: |
git fetch origin "refs/pull/${{ needs.prepare.outputs.pr_number }}/head"
git checkout --force FETCH_HEAD
- name: Load orchestrator prompt
id: orchestrator
env:
BASE_REF: ${{ github.base_ref || 'dev' }}
run: |
git fetch origin "$BASE_REF" --depth=1 2>/dev/null || true
ORCH_PROMPT=$(git show "origin/${BASE_REF}:.github/review-prompt.md" 2>/dev/null || echo "")
if [ -z "$ORCH_PROMPT" ]; then
echo "::warning::review-prompt.md not found — using fallback merge"
ORCH_PROMPT="Merge the 3 review outputs below into a single unified review. Deduplicate findings by file:line (highest severity wins). Produce a final assessment: APPROVED, APPROVED WITH NOTES, or CHANGES REQUESTED."
fi
DELIM="ORCH_$(openssl rand -hex 16)"
echo "prompt<<${DELIM}" >> "$GITHUB_OUTPUT"
printf '%s\n' "$ORCH_PROMPT" >> "$GITHUB_OUTPUT"
echo "${DELIM}" >> "$GITHUB_OUTPUT"
- name: Prepare review inputs
id: review-inputs
run: |
SECURITY=$(cat security_review.md 2>/dev/null || echo "Security review not available.")
QUALITY=$(cat quality_review.md 2>/dev/null || echo "Quality review not available.")
CCS=$(cat ccs_review.md 2>/dev/null || echo "CCS compliance review not available.")
# Write combined input file for orchestrator
{
echo "## Security Review Output"
echo ""
printf '%s\n' "$SECURITY"
echo ""
echo "---"
echo ""
echo "## Quality Review Output"
echo ""
printf '%s\n' "$QUALITY"
echo ""
echo "---"
echo ""
echo "## CCS Compliance Review Output"
echo ""
printf '%s\n' "$CCS"
} > review_inputs.md
- name: Run Orchestrator Merge
id: claude-merge
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.GLM_API_KEY }}
github_token: ${{ steps.app-token.outputs.token }}
show_full_output: true
track_progress: false
prompt: |
think
You are the review orchestrator for PR #${{ needs.prepare.outputs.pr_number }} in ${{ github.repository }}.
PR HEAD SHA: ${{ needs.prepare.outputs.head_sha }}
CONTRIBUTOR: @${{ needs.prepare.outputs.author_login }}
${{ needs.prepare.outputs.contributor_source == 'external' && 'EXTERNAL CONTRIBUTOR PR.' || 'INTERNAL PR.' }}
${{ steps.orchestrator.outputs.prompt }}
## Review Inputs from 3 Parallel Reviewers
Read the file `review_inputs.md` for the raw outputs from all 3 reviewers (security, quality, CCS compliance).
## IMPORTANT: Writing the Final Review
After merging and assessing, use the `Write` tool to write the final unified review to `${{ env.REVIEW_OUTPUT_FILE }}`.
Use `Write` tool directly — do NOT use `Edit`.
Do NOT post GitHub comments yourself. The workflow will publish the saved file.
Do NOT modify any source code files.
IMPORTANT: Do NOT use shell operators (|| &&) or heredoc (<<) in bash commands.
End your review with:
> Parallel review by `${{ env.REVIEW_MODEL }}` (3 focused reviewers + orchestrator merge)
claude_args: |
--bare
--model ${{ env.REVIEW_MODEL }}
--permission-mode bypassPermissions
--max-turns 15
- name: Fallback merge (if orchestrator fails)
if: always() && steps.claude-merge.outcome != 'cancelled'
run: |
if [ -s "$REVIEW_OUTPUT_FILE" ]; then exit 0; fi
echo "::warning::Orchestrator merge failed — using simple concatenation fallback"
SECURITY=$(cat security_review.md 2>/dev/null || echo "Security review not available.")
QUALITY=$(cat quality_review.md 2>/dev/null || echo "Quality review not available.")
CCS=$(cat ccs_review.md 2>/dev/null || echo "CCS compliance review not available.")
{
echo "# Parallel AI Code Review"
echo ""
echo "> [!] Orchestrator merge failed — raw reviewer outputs below."
echo ""
echo "---"
echo ""
echo "## Security Review"
echo ""
printf '%s\n' "$SECURITY"
echo ""
echo "---"
echo ""
echo "## Quality & Correctness Review"
echo ""
printf '%s\n' "$QUALITY"
echo ""
echo "---"
echo ""
echo "## CCS Compliance Review"
echo ""
printf '%s\n' "$CCS"
echo ""
printf '> Parallel review by `%s` (fallback — orchestrator unavailable)\n' "$REVIEW_MODEL"
} > "$REVIEW_OUTPUT_FILE"
- name: Publish review comment
if: always()
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REVIEW_MARKER: >-
<!-- ccs-ai-review
run:${{ github.run_id }}
attempt:${{ github.run_attempt }}
pr:${{ needs.prepare.outputs.pr_number }}
sha:${{ needs.prepare.outputs.head_sha }} -->
run: |
if [ ! -s "$REVIEW_OUTPUT_FILE" ]; then
echo "::error::No merged review content available"
exit 1
fi
{
printf '%s\n\n' "$REVIEW_MARKER"
cat "$REVIEW_OUTPUT_FILE"
} > "$REVIEW_COMMENT_FILE"
COMMENTS_JSON="$(gh api "repos/${{ github.repository }}/issues/${{ needs.prepare.outputs.pr_number }}/comments?per_page=100")"
COMMENT_ID="$(
printf '%s' "$COMMENTS_JSON" |
jq -r --arg marker "$REVIEW_MARKER" '.[] | select(.body | contains($marker)) | .id' |
tail -n 1
)"
if [ -n "$COMMENT_ID" ]; then
jq -Rs '{body: .}' < "$REVIEW_COMMENT_FILE" |
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" --method PATCH --input -
echo "[i] Updated review comment ${COMMENT_ID} for PR #${{ needs.prepare.outputs.pr_number }}"
else
jq -Rs '{body: .}' < "$REVIEW_COMMENT_FILE" |
gh api "repos/${{ github.repository }}/issues/${{ needs.prepare.outputs.pr_number }}/comments" --method POST --input -
echo "[i] Posted review comment for PR #${{ needs.prepare.outputs.pr_number }}"
fi
- name: Add success reaction
if: success() && github.event_name == 'issue_comment'
run: |
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
--method POST -f content=rocket
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Add failure reaction
if: failure() && github.event_name == 'issue_comment'
run: |
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
--method POST -f content=confused
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Upload merged review artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: merged-review-pr${{ needs.prepare.outputs.pr_number }}-run${{ github.run_id }}
path: ${{ env.REVIEW_OUTPUT_FILE }}
retention-days: 7
if-no-files-found: warn
- name: Cleanup
if: always()
run: rm -f "$REVIEW_COMMENT_FILE" "$REVIEW_OUTPUT_FILE" security_review.md quality_review.md ccs_review.md review_inputs.md