~ / tutoriais /hands-on-pr-100-gerado-por-agente-laravel $ _

Hands-on: meu primeiro Pull Request 100% gerado por agente em Laravel (com diff e revisão)

Lucas Souza Lucas Souza 13 min de leitura Tutoriais
Hands-on: meu primeiro Pull Request 100% gerado por agente em Laravel (com diff e revisão)

Liguei o agente. Fui tomar café. Voltei 43 minutos depois e tinha um PR aberto no GitHub com 380 linhas mexidas em 9 arquivos.

Eu não escrevi nenhuma delas. O loop foi inteirinho do agente: leu a issue, abriu o projeto, criou branch, escreveu código, rodou os testes, fez commit, abriu o PR. Eu só apertei enter no início.

Esse post é o relato cru desse experimento. Tem o setup do harness em Laravel + Claude Agent SDK + sandbox isolado, a task escolhida, o loop cronometrado de ponta a ponta, o diff comentado linha a linha, os 3 bugs que passaram pelo agente e quase pelo meu code review humano, o custo final em USD e o veredito sobre soltar isso em produção. Repositório público no final.

TL;DR

  • O que é: case study real de um PR em Laravel gerado 100% por um agente Claude Sonnet 4.6 dentro de um harness próprio.
  • Stack: Laravel 11, Claude Agent SDK (Python) + harness custom em PHP, Docker sandbox isolado, GitHub CLI.
  • Custo: $2,87 em tokens, 43 minutos, 12 iterações do loop, 380 linhas de diff.
  • Veredito honesto: ótimo pra PR, ainda não pra merge automático. 3 bugs reais escaparam.
  • Repositório: github.com/beerandcode/laravel-agent-pr-demo (público).

O setup: harness Laravel + Claude Agent SDK + sandbox

Antes do agente rodar, eu precisei montar três peças.

1. Claude Agent SDK como núcleo. O SDK em Python expõe o ciclo canônico de um agente: gather context → take action → verify work → repeat. Não é nada mágico, é exatamente esse loop. A documentação oficial chama isso de primitivas de agente e o repositório fica em anthropics/claude-agent-sdk-python. Eu rodo com Claude Sonnet 4.6, que custa $3 input / $15 output por 1M tokens segundo o pricing oficial.

2. Harness em PHP. Aqui é onde mora a engenharia. O harness é todo o código que o agente precisa em volta do modelo: gestão de contexto, ferramentas customizadas, política de retry, compactação, logging, controle de custo, sandbox. A frase que define isso é direta: "a raw model is not an agent. It becomes one once a harness gives it state, tool execution, feedback loops, and enforceable constraints" (Ken Huang, sobre o leak interno do Claude Code).

Eu expus pro agente sete ferramentas:

// app/Agent/Tools/AgentToolset.php
return [
    'read_file'        => new ReadFileTool($workspace),
    'write_file'       => new WriteFileTool($workspace),
    'list_dir'         => new ListDirTool($workspace),
    'grep'             => new GrepTool($workspace),
    'run_artisan'      => new ArtisanTool($sandbox), // pest, migrate, route:list
    'run_phpstan'      => new PhpstanTool($sandbox),
    'git'              => new GitTool($workspace, allow: ['add', 'commit', 'push', 'checkout']),
];

Todas rodam dentro do Docker, com filesystem montado read-write só na pasta do projeto, sem rede pra *.production.local. Isso importa. Sem sandbox o agente pode fazer php artisan migrate:fresh no banco errado e o seu dia acaba ali.

3. Sandbox isolado. Um container Docker dedicado, php:8.3-cli + composer + git + gh. O harness sobe o container por sessão, monta o repo em /workspace, executa as ferramentas via docker exec, e mata o container quando o loop termina. Custo de subir/derrubar: ~3 segundos.

A regra do sandbox veio direto de uma sugestão da Anthropic no design de harnesses para apps de longa duração: cada componente do harness codifica uma suposição sobre limites do modelo. Sem isolamento, você está supondo que o modelo nunca vai errar destrutivamente. E ele vai.

A task escolhida (e por que cabe num agente hoje)

A issue era essa, copiada literal do board:

#214 — Adicionar soft delete + audit log em OrderService

Hoje quando o cliente cancela um pedido o registro é apagado fisicamente. Precisamos manter o histórico (soft delete) e gravar quem cancelou, quando, e o motivo numa tabela order_audits. Endpoint atual: DELETE /api/orders/{id}. Mantém retrocompatibilidade.

Por que essa task cabe num agente hoje:

  • Escopo claro. Tem entrada (request), saída (status code + linhas em duas tabelas), e regra de negócio explícita.
  • Padrão repetível. Soft delete + audit é um dos patterns mais documentados do ecossistema Laravel. O modelo viu isso 10 mil vezes no treino.
  • Verificável em runtime. Existem testes Pest no projeto. O agente pode rodar pest e ler o resultado, não é "achismo", é feedback de máquina.
  • Sem ambiguidade de produto. Não precisa decidir UX, copy ou stack. Decisão técnica já está costurada.

O benchmark público mais honesto pra esse tipo de task é o SWE-bench Verified. O Sonnet 4.6 marca 79,6% no SWE-bench Verified, com a curva por duração mostrando 87% em tarefas de até 15 min, 75% em 15min-1h, 50% em 1-4h e 33% acima disso. Minha task estimada cai no segundo bucket. Probabilidade boa, mas longe de garantia.

O loop cronometrado: 43 minutos, 12 iterações

O harness loga cada iteração com timestamp, tokens consumidos, ferramentas chamadas e diff acumulado. Resumo direto do log:

# Tempo Tokens in/out O que o agente fez
1 00:00 18k / 1.2k Leu a issue, list_dir em app/ e tests/, mapeou estrutura
2 02:31 22k / 2.1k Leu OrderService.php, OrderController.php, migration de orders
3 05:48 35k / 3.4k Decidiu criar migration order_audits + trait Auditable
4 09:12 41k / 4.2k Escreveu a migration, rodou php artisan migrate no sandbox
5 12:30 48k / 5.1k Aplicou SoftDeletes no Order, criou model OrderAudit
6 17:55 63k / 6.8k Refatorou OrderService::cancel() pra gravar audit + soft delete
7 21:09 71k / 4.3k Rodou pest. 3 testes vermelhos
8 24:48 89k / 7.7k Leu falha, ajustou assert de pedidos cancelados nos testes existentes
9 28:12 102k / 5.5k pest passou. Rodou phpstan (level 8). 2 erros de tipo
10 32:40 119k / 6.9k Corrigiu tipos no audit + nullable no cancelled_by_id
11 36:33 132k / 8.1k Escreveu 4 testes novos cobrindo audit e soft delete. Tudo verde
12 41:02 148k / 11.4k git add, commit semântico, push, gh pr create com descrição

Total: 43 min real, 148k tokens input + 67k output ao longo do loop. Custo em dólar mais à frente.

O loop tem comportamento que vale destacar.

Na iteração 7, o agente bateu nos testes vermelhos. Ele não desistiu nem ficou alucinando. Leu o stack trace, identificou que o assert antigo esperava 404 no GET /api/orders/{id} após delete (porque o registro sumia fisicamente), e atualizou o teste pra esperar 410 Gone. Discutível? Sim. Burro? Não. Documentou a decisão no commit.

Esse é o tipo de comportamento que a Anthropic chama de "verify work" no ciclo do SDK. Rodar teste, ler saída, decidir se mexe no código ou no teste, agir, repetir. Sem isso é só geração assistida de texto.

O diff comentado, linha a linha do que importa

Vou destacar 4 trechos do diff que valem comentário.

1. Migration nova, boa.

// database/migrations/2026_05_11_120000_create_order_audits_table.php
Schema::create('order_audits', function (Blueprint $table) {
    $table->id();
    $table->foreignId('order_id')->constrained()->cascadeOnDelete();
    $table->foreignId('cancelled_by_id')->nullable()->constrained('users')->nullOnDelete();
    $table->enum('action', ['cancelled', 'refunded', 'archived']);
    $table->text('reason')->nullable();
    $table->json('snapshot');
    $table->timestamp('happened_at')->useCurrent();
    $table->timestamps();

    $table->index(['order_id', 'happened_at']);
});

Coisas que ele acertou: cascadeOnDelete na FK de order_id (faz sentido junto com soft delete: se você forçar forceDelete do pedido, o histórico vai junto, discutível mas consistente). snapshot em JSON pra guardar o estado pré-cancelamento. Índice composto pra query típica "histórico do pedido X em ordem cronológica".

2. Refactor do service, mediano.

// app/Services/OrderService.php
public function cancel(Order $order, User $actor, ?string $reason = null): Order
{
    return DB::transaction(function () use ($order, $actor, $reason) {
        OrderAudit::create([
            'order_id'        => $order->id,
            'cancelled_by_id' => $actor->id,
            'action'          => 'cancelled',
            'reason'          => $reason,
            'snapshot'        => $order->toArray(),
            'happened_at'     => now(),
        ]);

        $order->update(['status' => OrderStatus::Cancelled]);
        $order->delete(); // soft delete agora

        return $order;
    });
}

Transaction tá certo. O fluxo audit-antes-delete tá certo (se der erro depois, o audit não fica órfão). Mas tem detalhe ruim: $order->toArray() no snapshot inclui relations já carregadas, o que pode estourar JSON gigante dependendo de quem chama. Eager loading anterior vira parte do audit. Bug latente.

3. Endpoint mantido, exatamente como pedi.

// app/Http/Controllers/OrderController.php
public function destroy(Order $order, CancelOrderRequest $request): JsonResponse
{
    $this->orderService->cancel($order, $request->user(), $request->validated('reason'));

    return response()->json(null, 204);
}

Manteve DELETE /api/orders/{id} e o 204 no sucesso. Retrocompatibilidade aparentemente ok. Criou um CancelOrderRequest pra validar o reason opcional. Decisão boa.

4. Testes novos, competentes.

// tests/Feature/CancelOrderTest.php
it('records an audit row when an order is cancelled', function () {
    $user  = User::factory()->create();
    $order = Order::factory()->create();

    actingAs($user)
        ->deleteJson("/api/orders/{$order->id}", ['reason' => 'cliente desistiu'])
        ->assertNoContent();

    expect(OrderAudit::query()->where('order_id', $order->id)->count())->toBe(1);
    expect($order->fresh()->trashed())->toBeTrue();
});

Teste é objetivo, usa actingAs e assertNoContent, valida tanto o efeito colateral (linha no audit) quanto o estado final (trashed). Não testou reason chegando no audit, mas é nitpick.

Os 3 bugs que escaparam

Aqui é onde o post deixa de ser propaganda do agente.

Bug 1, race condition no audit. A transaction está no nível do PHP, mas o OrderAudit::create usa now() no PHP, não DB::raw('NOW()'). Em ambiente com filas processando cancelamentos em paralelo, dois jobs concorrentes podem gravar audits com timestamps invertidos em relação à ordem real das transactions no Postgres. O harness não me reportou isso porque os testes rodam em SQLite single-thread. Eu peguei revisando.

Bug 2, snapshot vaza relations. Citei no diff acima: $order->toArray() inclui qualquer with(...) que o caller tenha feito antes. Um endpoint que faz Order::with('items.product.media')->find(...) vai gravar um snapshot de ~40 KB por audit. Não é fim do mundo, mas estoura disco em alguns meses sem ninguém perceber. O fix correto é serializar só o getAttributes() do model ou definir um auditable array explícito.

Bug 3, assertNoContent é mentira na rota antiga. O controller velho retornava 200 com { "deleted": true }, não 204. O agente leu o pedido da issue ("manter retrocompatibilidade"), criou um teste novo com 204, e não checou que clientes existentes esperavam o JSON do 200. Quebra silenciosa de contrato. Nenhum teste de regressão captou porque o teste antigo foi atualizado pelo próprio agente na iteração 8.

Esse terceiro bug é o mais perigoso, e ele ilustra o ponto que a Anthropic faz no harness design: você não pode confiar que o agente vai "verificar o próprio trabalho" se ele também controla o que conta como verificação. Precisa de eval externo, contract test, ou um juiz separado.

A boa notícia: um eval de contrato com 50 casos chamando o endpoint antigo e comparando status_code + body teria pegado isso na primeira rodada. É barato fazer. É a lacuna mais óbvia do meu harness atual.

Custo: $2,87 vs ~$110 do dev sênior

Conta do agente:

item qtd preço unitário total
input tokens (sem cache) 148.000 $3 / 1M $0,444
input tokens (cache hit, 10%) 612.000 $0,30 / 1M $0,184
output tokens 67.000 $15 / 1M $1,005
reasoning tokens 80.000 $15 / 1M $1,200
total $2,87

Sim, sem prompt caching agressivo o número triplica fácil. O cache hit a 10% do preço de input é o que torna esse tipo de loop viável.

Conta equivalente do dev sênior: 1h de trabalho ponta a ponta (issue até PR aberto), valor de mercado conservador de $80 a $120/h pra pleno-sênior no Brasil. Chama de $110 médio. Diferença bruta de ~38x.

Mas essa conta é incompleta. Adiciona:

  • 25 min meus revisando o PR e achando os 3 bugs.
  • 15 min escrevendo o eval de contrato que vou pôr no CI.
  • Custo emocional de explicar pro time que sim, o PR foi gerado por agente, e não, não é golpe de produtividade.

Recalcula com 40 min meus a custo de hora cheia: agente ~$2,87 + ~$80 humano = $82,87 total, contra $110 do humano sozinho. A diferença real fica em ~25%, não em 38x. Continua valendo, mas a manchete é mentira.

Veredito: produção ainda não, mas perto

Resumo do que o experimento mostrou:

  • PR-quality, não merge-quality. O código do agente serve como ponto de partida sólido. Não serve como merge automático. Os 3 bugs encontrados não são corner case, são o tipo de coisa que aparece em quase todo loop.
  • O harness importa mais que o modelo. Trocar Sonnet 4.6 por Opus 4.7 sobe a qualidade marginal e o custo desproporcionalmente. O que destrava produção é melhor sandbox, evals externos, e ferramentas de verificação que o agente não controla.
  • A curva de "tasks que cabem hoje" é estreita e crescente. Soft delete + audit cabe. Refactor cross-module com decisões de arquitetura, não. Bug de produção com pouca informação, não. Mas a fronteira mexe rápido.

Se você quer ver esse mesmo loop sendo construído ao vivo, com o código rodando e as decisões de arquitetura sendo tomadas na hora, é exatamente isso que destrincho no Harness Engineering com Claude Code: a partir do loop cru do SDK até um agente que aguenta produção, sem slide de hype.

O repositório do experimento está aberto em github.com/beerandcode/laravel-agent-pr-demo, com o harness, os logs das 12 iterações, e os 3 bugs marcados como issues. Clona, roda, quebra. Esse é o caminho.

FAQ rápido

Por que Sonnet 4.6 e não Opus 4.7? Pra esse perfil de task (escopo claro, padrão repetível, verificável), Sonnet 4.6 fica 1,2 ponto atrás no SWE-bench Verified por 5x menos. Não compensa pagar Opus. Pra tasks de 1-4h de complexidade, a conta inverte.

Posso usar Claude Code direto em vez de montar harness? Pode, e provavelmente deveria, se o seu uso é interativo no terminal. O Claude Code é um harness pronto. Faz sentido montar o seu quando você precisa de agente rodando sem humano (CI, fila, cron), com ferramentas específicas do seu domínio ou políticas internas. O SDK é justamente pra esse caso.

E quando o agente alucina o nome de uma função? Acontece. No meu loop foi 1 vez (chamou Auditable::recordSnapshot que não existe). O grep rodando como ferramenta de verificação no passo seguinte mostrou o erro, e o agente se corrigiu em 1 iteração. Sem grep como tool, viraria invenção plausível e sairia no PR.

Custo escala linear com o tamanho da task? Não. Cresce mais que linear porque o contexto acumula. Tarefa de 2x o tamanho costuma custar 3-4x. A solução da própria Anthropic é decompor em sprints com context resets entre eles, em vez de empurrar tudo num único loop.

O experimento próximo é o oposto deste: mesma task, mesmo harness, mas com um eval de contrato no loop antes do PR ser aberto. Aposta que o bug 3 não passa. Posto o resultado.

Lucas Souza
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

Você também pode gostar

Agentic Code: o que muda quando o agente escreve, executa e testa o próprio código
Notícias

Agentic Code: o que muda quando o agente escreve, executa e testa o próprio código

Vibe coding deixou o dev no volante. SDD desenhou o mapa. Agentic Code tira o dev do carro e dá a chave pro agente, com freio de mão na mão. Cunhagem do termo em PT-BR, taxonomia de 4 níveis de autonomia, anatomia do ciclo plan/act/observe/reflect, demo comparativa de CRUD em três paradigmas, modos de falha reais e o que o harness precisa garantir pra rodar agente em produção sem quebrar tudo.

· 11 min
SDD do zero em Laravel: transformando uma feature real em specification executável
Tutoriais

SDD do zero em Laravel: transformando uma feature real em specification executável

Vibe coding com agente em Laravel funciona até a feature ter regra de negócio. Aí o agente inventa. Spec-Driven Development resolve isso virando a especificação na fonte da verdade. Neste post a gente percorre o ciclo PRD, spec, plan, tasks, código e testes em uma feature aparentemente boba: exportar relatório de vendas em PDF. Stack PHP, Claude Code e Spec Kit, do zero.

· 8 min
Construindo seu primeiro harness em Laravel: do prompt isolado ao loop autônomo
Tutoriais

Construindo seu primeiro harness em Laravel: do prompt isolado ao loop autônomo

Construa do zero um harness em Laravel mais Claude API: um service PHP que recebe a tarefa, escolhe qual tool chamar, executa em loop ate concluir e reporta. Inclui handling de erros com is_error, limite de iteracoes e logging real. Codigo executavel, sem framework de agente.

· 4 min
LLM-as-a-Judge: avaliação automatizada do seu agente de ofertas sem abrir planilha
Tutoriais

LLM-as-a-Judge: avaliação automatizada do seu agente de ofertas sem abrir planilha

Como montar um juiz LLM que pontua cada resposta do agente contra uma rubrica objetiva: preço correto, link válido, sentimento de review coerente. Você sai do achismo e transforma iteração em ciclo mensurável.

· 11 min

VirguIA

beer & code assistant

conectando…

Não foi possível iniciar o chat agora.

tocando