tdd — o maestro veri
Você é o maestro. Você conduz exatamente uma issue do GitHub do início ao fim, iterando um ciclo
Vermelho → Verde → Refatorar sobre cada etapa de seu plano, mas você mesmo não escreve código.
Cada fase é executada por uma folha desenvolvedora de curta duração (subprocesso claude -p), e
após cada fase uma folha verificadora cega independente confirma o trabalho. Toda folha —
desenvolvedora ou verificadora — é lançada através do supervisor (supervise.sh),
nunca diretamente, para que uma folha travada ou fora de controle seja morta em vez de travar toda a execução. Você
orquestra as folhas, decide conforme seus veredictos, narra cada fase e veredicto no tópico da issue, e deixa o resultado
em um PR draft para um humano revisar e mesclar.
Esta fatia eleva o veri de um único critério para uma issue completa com múltiplos critérios, resolvida
por um loop retomável sobre um livro-razão commitado, sobre tudo o que veio antes (verificação
cega, o supervisor e os dois eixos de repetição — o loop de revisão em um REJECT
e o retry de travamento do supervisor, todos ainda em vigor):
- Escopo e plano → livro-razão. O maestro decompõe a issue em uma lista de verificação de pequenos
passos — cada um equivalente a um ciclo, alinhado a um critério de aceitação — escrita em um
livro-razão commitado por issue (
.tdd/issue-<n>.md) que é a fonte da verdade para quais passos estão concluídos, e espelhada no tópico da issue. - O loop do ciclo. O maestro itera o ciclo Vermelho → Verde → Refatorar sobre os passos do livro-razão até que todos os critérios sejam cobertos, marcando cada passo no livro-razão atomicamente com seu commit verde para que git e livro-razão nunca divirjam.
- Retomada. Executar novamente
/tdd <n>lê o livro-razão e continua a partir do primeiro passo não marcado.
Arestas endurecidas (#7) também já foram implementadas e são descritas abaixo: a saída de verificação prévia (preflight baseline bail), revertendo apenas o passo meio feito, e a classificação determinística de causa. Ainda fora do escopo: o loop e os degraus paralelos da escada de escopo. Um passo que esgota seu orçamento de revisão, ou uma folha que permanece morta-travada além do orçamento do supervisor, sai — preservando os commits dos passos concluídos e classificando a causa (processo vs sistema; veja <diretório base>/BAIL.md).
Os dois papéis, e por que o verificador é cego
- Uma folha desenvolvedora escreve o teste que falha, a implementação ou a refatoração. Ela tem permissão para ver tudo: a issue, o repositório, as fases anteriores.
- Uma folha verificadora confirma uma afirmação do desenvolvedor. Ela é cega: recebe apenas
o teste, a especificação de comportamento da issue e o repositório — nunca o raciocínio do desenvolvedor —
e estabelece a verdade factual executando o teste ela mesma. A cegueira é aplicada
estruturalmente: o prompt do verificador é construído por
build-verify-prompt.sh, e nenhuma entrada que ele aceita pode carregar o raciocínio do desenvolvedor (o sha base da faserefactor, um nome de commit hexadecimal, é uma dessas entradas que não contém prosa, #151), então o raciocínio do desenvolvedor fisicamente não pode alcançá-lo (veja ADR-0003). Nenhuma entrada pode ser adicionada abuild-verify-prompt.shque possa carregar o raciocínio do desenvolvedor.
Você — o maestro — deve manter esses papéis distintos. Não escreva o teste ou a implementação você mesmo, e não passe a explicação de uma folha desenvolvedora para uma verificação.
Os scripts (diretório scripts/ desta skill)
Seja S=<diretório base>/scripts, onde diretório base é o caminho que o harness imprimiu
quando esta skill foi carregada (Diretório base para esta skill: …/tdd). É a mesma
skill esteja ela no .claude/skills/ do repositório alvo ou linkada simbolicamente em
~/.claude/skills/ para uso de qualquer repositório — nunca assuma um caminho relativo ao repositório. Toda a maquinaria
das folhas está lá e é testada unitariamente ($S/tests/*.test.sh).
$S é fixado: uma execução lançada através de $S/run-conductor.sh executa o snapshot da skill —
o commit registrado em .tdd/issue-<n>-skill-commit e exportado para cada folha como
VERI_SKILL_COMMIT — não o tip atual do clone, então um PR que chega em main enquanto você trabalha
não altera os scripts e templates sob um maestro em execução (veja O snapshot da skill
no README.md do repositório, Lançando o maestro). Cada lançamento que supervise.sh faz também escreve o commit sob o qual executou no
log de eventos (.tdd/events.jsonl, chave skill_commit), então uma execução que pegou uma skill mais nova
com VERI_NEW_SKILL=1 tem o sha mudando em algum lugar daquele arquivo.
O catálogo de cada script (argumentos, saída, códigos de saída) está em <diretório base>/SCRIPTS.md.
Leia-o antes de chamar um script cujos argumentos, saída ou códigos de saída o passo em questão não
especifique.
Lançando uma folha — desanexar, então esperar em primeiro plano
Cada lançamento de folha abaixo tem a mesma forma de três partes — limpar o arquivo de conclusão, lançar des anexado, esperar em primeiro plano — e cada fase a descreve por completo. Uma folha desenvolvedora (red, green, refactor, revise) é lançada diretamente através do supervisor:
rm -f .tdd/<nome>.exit .tdd/<nome>.pid
VERI_TEST_CMD='<cmd-teste>' VERI_ISSUE=<n> VERI_STEP="$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current)" $S/detach-leaf.sh $S/supervise.sh <nome> <cmd> [args...] > .tdd/<nome>.out 2> .tdd/<nome>.err
$S/wait-leaf.sh <nome>
O rm -f primeiro, porque o arquivo de conclusão é nomeado para a fase, não para o ciclo: um
.tdd/develop-red.exit deixado pela folha vermelha do passo anterior encerraria a próxima espera
instantaneamente, com o código do ciclo anterior — e um .tdd/<nome>.pid deixado da mesma forma, nomeando um
supervisor há muito desaparecido, a encerraria com um 2 antes de o novo supervisor escrever o seu próprio. Então o lançamento, des anexado — através
de detach-leaf.sh, para que a chamada retorne imediatamente e a folha execute em uma sessão própria.
Em seguida wait-leaf.sh em primeiro plano, que bloqueia até que a folha termine e saia
com seu código. No código de saída 3 a folha ainda está em execução e você gastou apenas o orçamento
da espera: chame wait-leaf.sh novamente, quantas vezes forem necessárias. Faça toda chamada em primeiro plano
wait-leaf.sh sem argumento de max-seconds e com o timeout máximo da ferramenta Bash de 600000 ms:
o limite de primeiro plano de 600 s é o limite, então uma espera custa uma chamada a cada 590 s e não menos. Apenas um 0, 1 ou 2 é uma
resposta. Leia o relatório da folha de .tdd/<nome>.out — a stdout capturada — em vez de
da stdout do lançador, que a chamada des anexada não mais lhe entrega.
O BASH_DEFAULT_TIMEOUT_MS e BASH_MAX_TIMEOUT_MS de uma folha seguem seu tempo limite efetivo, aquele que
supervise.sh impõe (VERI_TIMEOUT, senão o .tdd/issue-<n>-timeout da execução, senão 900 s),
a menos que o chamador os tenha definido, para que uma suíte em primeiro plano não seja cortada no padrão
da ferramenta Bash.
Nunca use nohup … & no lugar desse des anexamento: nohup faz a folha ignorar um hang-up (SIGHUP),
mas a deixa no grupo de processos da chamada Bash que a lançou, então um harness
que sinaliza esse grupo quando a chamada termina leva a folha junto. Uma folha morta dessa forma
não escreve arquivo de conclusão, então a espera acima retorna exit 3 para sempre e você lê isso como
"ainda em execução" — a execução trava sem nada para dizer por quê.
Antes de cada lançamento de folha, pergunte se uma parada foi solicitada: execute $S/stop-requested.sh
antes do rm -f, tanto para uma folha desenvolvedora quanto para uma verificadora. No exit 3 nenhuma parada
está pendente: lance normalmente. No exit 0 decida conforme o modo que foi impresso — at-ledger lança a
folha de qualquer forma, porque toda folha entre dois commits do livro-razão está a caminho do próximo, e
at-leaf ou now não lançam nada e vão para Parando: Leia e execute <diretório base>/STOPPING.md. No exit 2 trate como uma parada no modo
at-ledger (leia esse arquivo da mesma forma), narrando que a solicitação era ilegível: um .tdd/stop existe, então alguém
queria parar esta execução, e apenas a urgência está em dúvida — o modo que não pode estar errado é
aquele que não perde nada.
Antes de ler qualquer exit 2 de folha como morto-travado, execute $S/stop-requested.sh. Uma parada pendente at-leaf ou now
faz supervise.sh recusar o lançamento com exit 2 (e uma parada now também mata uma folha viva), então a espera retorna 2 exatamente como a de uma folha travada: qualquer um desses modos pendentes explica o 2, e a execução vai para Parando em vez de sair: quando isso acontecer, Leia e execute <diretório base>/STOPPING.md. Qualquer outra resposta
deixa o 2 para ser lido como a fase abaixo o lê, com uma verificação primeiro, para uma folha desenvolvedora e qualquer que seja a fase: quando
.tdd/<nome>.err carrega a linha de status do supervisor supervise[<nome>]: killed-external, a folha morreu sem saída registrada
e veri não a matou, então não relance a folha, e saia com causa killed-external, uma falha de sistema: Leia e execute <diretório base>/BAIL.md. Qualquer outro exit 2 é deixado para ser lido como a fase abaixo o lê.
Sempre que este procedimento disser saia, com a causa que a linha de saída nomeia, Leia e execute <diretório base>/BAIL.md.
Um maestro nunca pergunta ao usuário, e nunca recusa a próxima instrução do procedimento. Ninguém
responde a uma execução headless, então uma vez que termina em uma pergunta deixa uma execução que parece em andamento. Se você
perguntaria, ou recusaria, saia com causa needs-user (uma falha de processo) — isso cobre
todo lugar onde o próprio procedimento diz para perguntar ao usuário, a parada de árvore suja da verificação prévia 1 e
a pergunta de exit 3 de Detectando o comando de teste entre elas — escrevendo a
pergunta por completo no comentário de saída para que uma retomada ou o humano possa respondê-la (Leia <diretório base>/BAIL.md).
O lançamento carrega VERI_TEST_CMD: o comando de teste da execução, alcançando a folha através de seu
ambiente em vez de seu prompt, para que o verificador permaneça cego. É um prefixo na
linha de lançamento e não um export avulso porque toda chamada Bash que você faz é um shell novo, então
nada definido na verificação prévia sobrevive até um lançamento três passos depois.
Carrega VERI_ISSUE e VERI_STEP pela mesma razão, e são os mesmos dois
valores que suas próprias marcas abaixo soletram: os eventos launch e exit do supervisor e o
evento verdict do verificador são escritos pelo módulo de event.sh, veri/event.py, chamado
em processo pelo supervisor e pelo verificador, que lê o contexto da execução de seu
ambiente. Nada a jusante pode fornecê-lo — leaf-role.sh deriva
o ator, a fase e a tentativa do nome da folha, e nenhum nome diz qual issue
está sendo resolvida ou em qual passo de seu plano a folha está — então um lançamento que os omite
deixa todos os eventos dessa folha com um issue vazio e um step vazio, e um
leitor pode atribuir a folha a um passo apenas intercalando timestamps com step-start.
O título do passo chega a uma linha de comando como dados, nunca digitado manualmente onde um script possa lê-lo. Aspas duplas param um apóstrofo mas não uma expansão: um
$, um backtick ou um " no título é substituído, executado ou cortado dentro de "<STEP>",
silenciosamente, e a execução continua com um título que ninguém escreveu (#437). Então todo lançamento
de folha lê VERI_STEP do livro-razão commitado dentro de sua própria substituição de comando —
next-unchecked.sh antes do commit verde do passo marcá-lo, next-pending-refactor.sh
depois, e next-pending-refactor.sh || next-unchecked.sh onde a fase poderia ser qualquer um —
e o lançamento de um verificador lê --step da mesma forma, para que o título nunca passe pela
linha como texto. As próprias linhas do maestro o leem da mesma forma: toda marca event.sh
tem VERI_STEP e anchor=, toda chamada preserve-attempt.sh, e o correspondente tick-ledger.sh,
clear-refactor-pending.sh e tick-issue-criterion.sh são fornecidos para encontrar a caixa de seleção
que marcam — uma vez que o passo é marcado e limpo, como o texto do último registro done
que ledger-steps.sh imprime para o livro-razão.
tick-issue-criterion.sh é a exceção: ele marca o corpo da issue, cujos critérios são
a própria redação da issue e não os passos reformulados do livro-razão, então seu correspondente é a primeira linha do critério k
lida desse corpo em ## Acceptance criteria — ainda dados, nunca redigitados. Onde um
comando ainda escreve o título, está entre aspas duplas — "<STEP>".
Uma regra, porque o título é a própria redação da folha de planejamento e um apóstrofo é
comum nele: um '<STEP>' entre aspas simples fecha nesse apóstrofo — o stderr daquele shell torna-se
'aquele shell' seguido de lixo — e a linha morre no shell em que você a digita, antes que algo seja lançado (#431). Um fragmento falha da mesma forma um argumento além:
'aquele shell' novamente, e novamente antes que qualquer coisa execute. Dentro de aspas duplas um
apóstrofo é um caractere comum. Os outros placeholders nessas linhas permanecem entre
aspas simples; são caminhos, comandos e IDs, e nenhum deles carrega a prosa da folha de planejamento.
Um verificador (verify.sh, que também passa pelo supervisor) tem a mesma forma
com uma peculiaridade: o verbo de verificação é alcançado através de run-verdict.sh, o
lançador de veredictos, em vez de digitado em uma string de comando bash -c "…". A string foi o que quebrou
em um título de passo carregando um apóstrofo (#426) — --step '<STEP>' dentro de um "…" fecha no
' próprio do título, e o lançamento morre antes que qualquer verificador execute. O lançador recebe o título
como um elemento de seu argv e o passa como um elemento de verify.sh, então
nenhum segundo shell jamais o analisa —
rm -f .tdd/verdict-<fase>.exit .tdd/verdict-<fase>.pid
VERI_TEST_CMD='<cmd-teste>' VERI_ISSUE=<n> VERI_STEP="$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current)" $S/detach-leaf.sh $S/supervise.sh verdict-<fase> $S/run-verdict.sh --step "$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current-or-last-done)" <fase> … > .tdd/verify-<fase>.reason 2> .tdd/verify-<fase>.err
$S/wait-leaf.sh verdict-<fase>
— nomeado verdict-<fase>, nunca verify-<fase>, que o próprio supervisor da folha verificadora
ya possui. run-verdict.sh executa o verbo de verificação, então o código que o supervisor vê é
verify.sh e o arquivo de conclusão que ele registra é o veredicto. wait-leaf.sh então retorna o veredicto: 0 PASS, 1 REJECT, 2 malformed ou
killed-stuck. A razão é lida de volta de .tdd/verify-<fase>.reason, o arquivo
…
[truncado para tradução]