O que o Prompt 03 Entrega
O Stop Hook que substitui o stub pelo engine real do loop
O Prompt 03 é o maior salto do projeto: o stub incondicional vira o engine que controla o ciclo de vida inteiro. A máquina de estados construída no Prompt 02 ganha um cérebro. O hook passa a ler o estado e tomar decisões BLOCK/APPROVE por fase.
O Stop Hook não é apenas um porteiro. Quando retorna BLOCK, entrega um reason que o Claude Code mostra ao Claude como instrução para o próximo turno. É um canal unidirecional: hook → Claude.
# APPROVE: turno termina normalmente
{"decision": "approve"}
# BLOCK: Claude recebe instrução para agir antes de encerrar
{"decision": "block", "reason": "### PR Reviewer — Round 1 of 3\n\nRun the reviewer script:\n\`\`\`bash\nbash runners/PR-123-round-1.sh\n\`\`\`\n\nThen read findings from state/review.yaml and advance to round 2."}
O Prompt 03 Completo
Cole este texto no Claude Code com o SPEC.md em contexto
Prompt 03 — Cole no Claude Code
Read SPEC.md — the lifecycle table is your implementation guide.
You are building Prompt 03: replace the fail-open stub in
`hooks/stop-hook.sh` with the real lifecycle engine.
Source `state/state-helpers.sh` at the top.
**Build the lifecycle engine:**
1. **ERR trap first** — unconditional fail-open on line 1 of active code:
`trap 'echo "[pr-reviewer] ERR line $LINENO" >&2; printf '"'"'{"decision":"approve"}'"'"'\n'; exit 0' ERR`
2. **Source state-helpers** (fail-open if missing):
`source state/state-helpers.sh 2>/dev/null || { printf '{"decision":"approve"}\n'; exit 0; }`
3. **Check if a review is active** — read status field.
If null/empty: approve silently.
4. **Case statement by status:**
SETUP:
- Run scripts/setup-worktree.sh $pr_number
- CAS: status → REVIEWING, round → 1, persona → Author, version++
- Write runner script to runners/PR-$pr_number-round-1.sh
- BLOCK with: "Run `bash runners/PR-$pr_number-round-1.sh` then
return to continue the review."
REVIEWING:
- If round >= max_rounds: CAS status → SUMMARIZING, persona → null
BLOCK with: "All rounds complete. Consolidate findings from
state/review.yaml and generate the review report."
- Else: increment round. Set persona based on round
(1=Author, 2=Reviewer, 3=Security).
Write next runner script.
BLOCK with runner instructions.
SUMMARIZING:
- CAS status → DONE
- APPROVE with no reason (silent)
DONE:
- Run scripts/cleanup-worktree.sh $pr_number
- APPROVE
default:
- Log unknown status, approve.
5. **write_runner helper** — writes self-contained bash to runners/:
- Gets PR diff: `cd $worktree_path && git diff origin/main...HEAD`
- Gets PR metadata: `gh pr view $pr_number --json title,body`
- Writes prompt to runners/PR-NNN-round-N.prompt (heredoc, single
quotes on delimiter to prevent expansion)
- Runner invokes: `claude --print < runners/PR-NNN-round-N.prompt`
- Runner output appended to findings in state/review.yaml
**Constraints:**
- ERR trap must be the FIRST active line
- BLOCK reason must be valid Markdown (Claude reads it as instructions)
- JSON escape the reason via python3 or sed fallback
- Never block the user — every code path either returns approve or block
**Smoke test (provide the commands):**
Create a test git repo with a PR branch and run:
- `echo '{"status":"SETUP","pr_number":1,"version":0}' |
[write to state/review.yaml]`
- Execute hook manually: `bash hooks/stop-hook.sh`
- Verify: output is BLOCK JSON, state is now REVIEWING, runner written
- Set state to DONE, re-run hook: output must be APPROVE
Report the smoke test results.
A Máquina de Estados
SETUP → REVIEWING → SUMMARIZING → DONE — transições explícitas
Cada estado tem uma única condição de transição. Não há transições implícitas ou "se não sei o que fazer, vou tentar tudo". Isso é o que torna o loop debugável.
O Runner Script
Como o hook passa o diff e contexto do PR ao Claude
O runner script é o ponto de integração entre o hook e o modelo. Ele monta o contexto completo — diff do PR, metadados do GitHub, instrução da persona — e invoca claude --print com esse contexto.
#!/usr/bin/env bash
# runners/PR-123-round-2.sh — Reviewer persona
set -euo pipefail
trap 'exit 0' ERR
PR_NUMBER=123
WORKTREE_PATH=/tmp/pr-review-123
PERSONA="Reviewer"
# 1. Obter diff do PR no worktree isolado
PR_DIFF=$(cd "$WORKTREE_PATH" && git diff origin/main...HEAD 2>/dev/null || echo "")
# 2. Obter metadados do PR
PR_META=$(gh pr view $PR_NUMBER --json title,body,files 2>/dev/null || echo "{}")
# 3. Montar prompt e invocar Claude
claude --print <<'PROMPTEOF'
You are reviewing PR #123 as a **$PERSONA**.
[persona instructions here...]
## PR Diff
[diff content]
## PR Metadata
[gh pr view output]
Output findings as YAML list:
findings:
- file: path/to/file
line: 42
severity: HIGH
description: ...
suggestion: ...
PROMPTEOF
Você pode testar o runner manualmente sem disparar o hook completo. Se a revisão produz output estranho, você sabe que o problema está no runner — não no hook, no estado ou no worktree.
Obtendo Contexto do PR
gh pr view e git diff — o contexto que torna a revisão significativa
Uma revisão genérica ("este código parece correto") é inútil. Uma revisão com contexto completo ("na linha 47 de auth.go, este token é enviado sem HTTPS_ONLY verificado, o que expõe credenciais") é acionável.
gh pr view — Metadados
gh pr view $PR_NUMBER \
--json title,body,files,author \
-q '{
title: .title,
description: .body,
files_changed: [.files[].path]
}'
git diff — O código em si
# No worktree isolado — não contamina working tree
cd "$WORKTREE_PATH"
git diff origin/main...HEAD \
-- '*.go' '*.ts' '*.py' \
':!*.lock' ':!*.sum'
PRs grandes podem ter diffs de milhares de linhas. Use git diff --stat primeiro para avaliar o tamanho. Para PRs muito grandes, filtrar por tipos de arquivo ou limitar com --diff-filter=AM (apenas arquivos adicionados ou modificados).
Verificação: Smoke Test com PR Sintético
Antes do Prompt 04 — validar o loop sem custo de API
O loop é a parte mais complexa. Testar com PR sintético (sem usar créditos de API) valida as transições de estado. Use claude --print mockado com um script que retorna findings fixos.
Criar repo de teste com PR sintético
Repo local com uma branch de feature e um commit simples. PR aberto no GitHub para que `gh pr view` funcione.
SETUP → REVIEWING
Inicializar estado com status=SETUP. Rodar hook. Verificar: output é BLOCK, estado mudou para REVIEWING, runner foi escrito em runners/.
REVIEWING round 1 → round 2
Rodar hook novamente. Verificar: round incrementou de 1 para 2, persona mudou para Reviewer, novo runner gerado.
DONE → APPROVE
Forçar estado para DONE. Rodar hook. Verificar: output é {"decision":"approve"}, worktree foi removido, estado limpo.
✅ Resumo do Módulo 4.4
Próximo Módulo:
4.5 — Prompt 04: Personas e Relatório — as 3 perspectivas e o output estruturado