MÓDULO 4.3

Prompt 02 — A Máquina de Estados

📚

Tópicos

6

⏱️

Minutos

50

🎯

Nível

Avançado

🔨

Tipo

Build

1

O que o Prompt 02 Entrega

Estado YAML persistente com primitivas de acesso seguro

O Prompt 02 constrói a base de estado do revisor: o arquivo YAML que persiste entre turnos do Claude Code, os helpers para ler/escrever com segurança e o mecanismo de lockfile. Sem isso, o hook do Prompt 03 não tem onde guardar informação.

💡 Deliverables do Prompt 02
1.state/review.yaml com schema completo e campos tipados
2.state/state-helpers.sh com read_state, write_state_atomic, cas_state
3.Mecanismo de lockfile via flock -n state/review.lock
4.Testes unitários dos helpers: read/write/CAS/lock em isolamento
2

O Prompt 02 Completo

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

Prompt 02 — Cole no Claude Code

02_state_machine.md
Read SPEC.md — use the schema defined there as source of truth.

You are building Prompt 02: the state persistence layer.
Do NOT modify any hook logic yet.

**Deliver in this prompt:**

1. `state/review.yaml` — initial template (empty review):
   pr_number: null
   worktree_path: null
   status: null            # SETUP|REVIEWING|SUMMARIZING|DONE
   round: 0
   max_rounds: 3
   persona: null           # Author|Reviewer|Security
   findings: []
   started_at: null
   version: 0              # CAS version — increment on every write

2. `state/state-helpers.sh` — source this in hooks and scripts:

   a) `read_state_field <field>` — reads a field from review.yaml using
      `grep "^field:" state/review.yaml | awk '{print $2}'`.
      Returns empty string if field missing or file absent.

   b) `write_state_atomic <yaml_content>` — writes content to
      state/review.yaml.tmp then renames to state/review.yaml.
      Creates state/ dir if absent.

   c) `cas_state <expected_version> <new_yaml_content>` — reads current
      version field. If it matches expected_version, calls
      write_state_atomic. If it doesn't match: prints CAS conflict to
      stderr and returns exit 1. Caller must handle.

   d) `acquire_lock` — runs `flock -n state/review.lock -c 'echo locked'`
      to test non-blocking. If another process holds the lock: prints
      error to stderr, returns exit 1.

   e) `release_lock` — removes state/review.lock. Idempotent.

3. Unit tests at `tests/test-state-helpers.sh`:
   - Write state, read it back — fields must match
   - CAS success: correct version → write succeeds
   - CAS conflict: wrong version → write fails, exit 1
   - Lock test: acquire → second acquire fails → release → acquire succeeds

**Constraints:**
- state-helpers.sh must be sourced, not executed directly
- write_state_atomic must use tmp + rename (never write directly)
- cas_state must never block — if version mismatch, fail immediately
- All helpers fail-open: errors log to stderr, never crash the caller

**Verification:**
- `bash tests/test-state-helpers.sh` runs all 4 tests and reports PASS/FAIL
- `source state/state-helpers.sh; write_state_atomic "pr_number: 42
version: 0"; read_state_field pr_number` prints "42"
- CAS test: set version to 0, call cas_state 99 ... — must fail

Tell me the test results (all 4 should PASS).
3

O Schema do Estado

PR number, worktree path, round, status, findings — cada campo com propósito

O schema não é apenas estrutura — é uma decisão de design. Cada campo responde uma pergunta específica que o hook vai precisar responder durante o loop.

Campo Tipo Pergunta que responde
pr_number int Qual PR estou revisando?
worktree_path string Onde está o código do PR?
status enum Em qual fase estou?
round int Qual rodada de revisão?
persona string Qual perspectiva usar nesta rodada?
findings list O que foi encontrado até agora?
version int Alguém modificou o estado desde que eu li?
Por que YAML e não JSON

YAML é legível por humanos sem ferramenta especial. Você pode abrir state/review.yaml num editor e entender exatamente o estado atual. JSON seria mais estrito, mas menos debugável quando algo dá errado às 23h com um PR urgente.

4

Escrita Atômica e CAS Aplicados ao Revisor

Estado que nunca corrompe, mesmo com interrupção

Claude Code pode ser interrompido a qualquer momento — Ctrl+C, timeout, crash. Sem primitivas de segurança, a interrupção durante uma escrita deixa o review.yaml corrompido e ilegível.

⚛️ Escrita Atômica via rename(2)
# ERRADO: escrita direta — se interrompida, o arquivo fica corrompido
echo "$new_content" > state/review.yaml

# CORRETO: escrever em tmp e renomear atomicamente
echo "$new_content" > state/review.yaml.tmp
mv state/review.yaml.tmp state/review.yaml
# mv/rename(2) é atômico no mesmo filesystem — não pode ser interrompido
🔄 Compare-and-Swap (CAS)
# CAS: só escreve se a versão que você leu ainda é a atual
cas_state() {
  local expected_version=$1
  local new_content=$2

  local current_version=$(read_state_field version)

  if [ "$current_version" != "$expected_version" ]; then
    echo "[pr-reviewer] CAS conflict: expected v$expected_version, got v$current_version" >&2
    return 1  # falha — o chamador deve relêr o estado e tentar novamente
  fi

  # Versão confere — pode escrever (atomicamente)
  write_state_atomic "$new_content"
}
5

Lockfile: Exclusão Mútua

Garantindo que apenas um revisor roda por vez no mesmo repositório

Se você abrir duas janelas do Claude Code no mesmo repositório, dois revisores podem ser disparados simultaneamente. Eles vão corromper o state/review.yaml um do outro. O lockfile previne isso.

🔐 flock não-bloqueante
# -n = non-blocking: falha imediatamente se o lock não está disponível
# -e = cria o lockfile se não existe
exec 9>state/review.lock
if ! flock -n 9; then
  echo "[pr-reviewer] Another reviewer is already running. Aborting." >&2
  exit 0  # fail-open: não bloqueia o usuário
fi
# Lock adquirido — o processo retém o lock até fechar fd 9 (EXIT trap)
trap 'flock -u 9; rm -f state/review.lock' EXIT

Stale Lock (lock fantasma)

Se o processo morreu sem liberar o lock, o arquivo state/review.lock existe mas não há processo segurando. O flock detecta isso automaticamente — um arquivo de lock sem processo = lock disponível.

Lock vs CAS

O lockfile garante exclusão mútua entre processos. O CAS garante consistência dentro de um processo contra interrupções. Ambos são necessários — são complementares, não alternativos.

6

Verificação: Testar CAS e Atomicidade em Isolamento

Antes de avançar para o Prompt 03

CAS e atomicidade são difíceis de debugar quando estão integrados com o hook completo. Testar agora, com o sistema simples, é a estratégia correta.

1

Teste de read/write

Escrever estado com write_state_atomic, ler com read_state_field, confirmar que todos os campos batem.

2

Teste de CAS bem-sucedido

Estado com version=0, chamar cas_state 0 com novo conteúdo — deve escrever. Verificar version=1 após.

3

Teste de CAS com conflito

Estado com version=0, chamar cas_state 5 — deve falhar com exit 1. Estado original deve permanecer inalterado.

4

Teste de lockfile

Adquirir lock em subshell, tentar adquirir em outra subshell — deve falhar. Liberar primeiro lock, segunda tentativa deve passar.

Todos os 4 testes devem passar antes de continuar

O arquivo tests/test-state-helpers.sh deve reportar 4/4 PASS. Se qualquer teste falha, o Prompt 03 vai construir sobre uma base quebrada.

Resumo do Módulo 4.3

Estado YAML — schema com 8 campos tipados, cada um com propósito explícito
Escrita atômica — tmp + rename, impossível de corromper por interrupção
CAS implementado — conflitos detectados e reportados sem corromper o estado
Lockfile non-blocking — exclusão mútua sem risco de deadlock

Próximo Módulo:

4.4 — Prompt 03: O Stop Hook e o Loop — o engine real da máquina de estados