MÓDULO 4.4

Prompt 03 — O Stop Hook e o Loop

📚

Tópicos

6

⏱️

Minutos

55

🎯

Nível

Avançado

🔨

Tipo

Build

1

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.

💡 BLOCK como canal de comunicação

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."}
2

O Prompt 03 Completo

Cole este texto no Claude Code com o SPEC.md em contexto

Prompt 03 — Cole no Claude Code

03_stop_hook_and_loop.md
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.
3

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.

Estado Quem Age Hook Faz
SETUP Plugin cria worktree CAS → REVIEWING, escreve runner round-1, BLOCK com instruções
REVIEWING Claude roda runner e lê diff Se round < max: incrementa, próximo runner, BLOCK. Se round ≥ max: CAS → SUMMARIZING
SUMMARIZING Claude consolida findings CAS → DONE, APPROVE silencioso
DONE Terminal Cleanup worktree, APPROVE
4

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.

🏃 Estrutura do Runner
#!/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
Por que separar runner do hook

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.

5

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'
📏 Gerenciando Diffs Grandes

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).

6

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.

1

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.

2

SETUP → REVIEWING

Inicializar estado com status=SETUP. Rodar hook. Verificar: output é BLOCK, estado mudou para REVIEWING, runner foi escrito em runners/.

3

REVIEWING round 1 → round 2

Rodar hook novamente. Verificar: round incrementou de 1 para 2, persona mudou para Reviewer, novo runner gerado.

4

DONE → APPROVE

Forçar estado para DONE. Rodar hook. Verificar: output é {"decision":"approve"}, worktree foi removido, estado limpo.

Resumo do Módulo 4.4

Stop Hook engine — case statement por estado, BLOCK com reason como instrução
Máquina de estados — 4 estados, transições explícitas e testadas
Runner separado do hook — testável e debugável independentemente
Smoke test validado — loop completo testado sem gastar créditos de API

Próximo Módulo:

4.5 — Prompt 04: Personas e Relatório — as 3 perspectivas e o output estruturado