Single page que recebe uma expressão cron e devolve, em português, o que ela significa e quando ela vai rodar de verdade.
Você digita 0 3 * * 1-5, clica em Explicar, e recebe:
- uma explicação em linguagem natural ("Às 3h de segunda a sexta.");
- as 5 próximas execuções com data e hora concretas, no timezone
America/Sao_Paulo.
Se a expressão for inválida, a página mostra qual campo está errado e o intervalo aceito por ele.
Este projeto foi construído ao vivo no workshop de Loop Engineering. Ele é ao mesmo tempo o produto e a demonstração do método: o código foi escrito por um loop de agentes dirigido por especificação. A segunda metade deste README explica como.
Deliberadamente pequeno: uma rota, um componente Livewire, um agente de IA, zero banco de dados de domínio, zero autenticação.
Não existe parser de cron em PHP. Toda a inteligência — interpretar a expressão, calcular as próximas datas, detectar erro e dizer qual campo está errado — vem de uma única chamada ao modelo via Laravel AI SDK com structured output.
Fora do MVP: histórico, persistência, contas de usuário, presets clicáveis, breakdown campo a campo, outros idiomas, geração de expressão a partir de texto (o caminho inverso), sintaxe estendida (Quartz, systemd timers).
Cinco campos separados por espaço — minuto, hora, dia-do-mês, mês, dia-da-semana — com *, valores únicos (30), listas (1,15), intervalos (1-5), passos (*/15, 10-30/5) e combinações (1,15-20,*/2).
Macros: @hourly, @daily, @weekly, @monthly, @yearly. Nenhuma outra.
| Camada | Tecnologia |
|---|---|
| Linguagem | PHP 8.5 (runtime Sail) |
| Framework | Laravel 13 |
| UI reativa | Livewire 4 + Blaze |
| IA | Laravel AI SDK (laravel/ai) — Lab::OpenAI |
| CSS | Tailwind CSS 4 via @tailwindcss/vite |
| Build | Vite 8 |
| Testes | Pest 5 |
| Análise estática | Larastan 3 |
| Formatação | Laravel Pint |
| Ambiente dev | Laravel Sail (serviço único, sem container de banco) |
| Persistência | Nenhuma |
routes/web.php Route::livewire('/', 'pages::cron-explainer')
resources/views/pages/⚡cron-explainer.blade.php SFC Livewire: estado, prompt, tratamento de erro e view
resources/views/pages/⚡cron-explainer.test.php testes co-locados do SFC
app/Ai/Agents/CronExplainer.php instruções do domínio + schema do structured output
config/cron-explainer.php modelo, timezone, quantidade de execuções, timeout
Toda a lógica vive no componente — não há Action nem service class. Ele monta o prompt (expressão + instante atual em ISO-8601 + timezone), chama o agente, valida o payload contra o contrato e traduz o resultado em estado de tela.
Contrato do structured output — quatro chaves, todas obrigatórias. Campos não aplicáveis vêm vazios ('' / []), nunca ausentes nem null:
{
"valido": true,
"explicacao": "A cada 15 minutos, entre 2h e 6h, apenas aos domingos.",
"erro": "",
"proximas_execucoes": [
"2026-08-23 02:00",
"2026-08-23 02:15",
"2026-08-23 02:30",
"2026-08-23 02:45",
"2026-08-23 03:00"
]
}Qualquer falha da chamada — rede, timeout, chave ausente ou inválida, payload fora do contrato, data não parseável — cai numa mensagem única na tela. O detalhe técnico vai para storage/logs, nunca para o usuário.
- O modelo não está no atributo
#[Model]. Só#[Provider(Lab::OpenAI)]é anotado; o identificador do modelo é passado em runtime a partir deconfig('cron-explainer.model'), para poder ser trocado por ambiente sem tocar em código. - Sem banco. A migration
agent_conversationsdolaravel/ainunca foi publicada — o agente é stateless, sem memória de conversa e sem tools. A suíte roda sem arquivo SQLite. OPENAI_API_KEYé zerada à força nos testes. Ophpunit.xmltem<env name="OPENAI_API_KEY" value="" force="true"/>. Sem oforce, a chave real do.env(ou exportada no shell) chegaria aos testes, e qualquer chamada que escapasse de um fake viraria requisição paga de verdade.tests/Feature/AmbienteDeTesteTest.phptrava esse invariante.- Risco conhecido e aceito: a IA calcula as datas, sem biblioteca determinística de cron. LLMs erram aritmética de calendário em casos como
0 0 31 * *(meses com menos de 31 dias) ou dia-do-mês combinado com dia-da-semana.
Estas e outras decisões estão registradas em .ai/rules/, indexadas por glob em .ai/rules/index.md — é lá que agentes e humanos leem as regras da casa antes de editar cada caminho.
Requer Docker.
git clone <repo> && cd workshop-cron-explainer
cp .env.example .env
vendor/bin/sail up -d
vendor/bin/sail composer install
vendor/bin/sail artisan key:generate
vendor/bin/sail npm install && vendor/bin/sail npm run buildPreencha OPENAI_API_KEY no .env e abra vendor/bin/sail open.
Variáveis do domínio:
| Variável | Padrão | Papel |
|---|---|---|
OPENAI_API_KEY |
— | credencial do provider |
CRON_EXPLAINER_MODEL |
gpt-5.6-terra |
modelo usado pelo agente |
CRON_EXPLAINER_TIMEZONE |
America/Sao_Paulo |
timezone das execuções exibidas |
CRON_EXPLAINER_TIMEOUT |
30 |
timeout, em segundos, da chamada à IA |
vendor/bin/sail composer test # config:clear + pint --test + phpstan + pest
vendor/bin/sail artisan test --compact
vendor/bin/sail bin pint --format agent
vendor/bin/sail composer types:checkOs testes usam CronExplainer::fake() — nenhuma requisição real é feita.
Atenção ao SFC: resources/views/ está fora dos paths do Larastan e o Pint padrão ignora *.blade.php. O único gate real do componente são os testes co-locados.
A parte do workshop. O produto acima é pretexto; o que se demonstra é o processo.
A ideia central: em vez de conversar com um agente até o código sair, você escreve a especificação, quebra em fases verificáveis, e deixa um loop executar as fases até cada uma passar no gate. O humano trabalha na spec e no gate — não no diff.
Tudo começa em .spec/init/, gerado por entrevista com o desenvolvedor:
.spec/init/project-description.md escopo, key concepts, stack, workflows, open questions
.spec/init/user-stories.md histórias testáveis
.spec/init/database-schema.md schema derivado (aqui: declarado intocado)
.spec/init/project-phases.md o plano de execução
project-description.md não é um documento decorativo: ele fixa o que está fora do MVP e registra as open questions — as decisões que ainda eram risco no momento do planejamento (o identificador do modelo não verificado, a precisão de datas calculadas por LLM, as migrations do SDK). O agente executa contra esse documento.
project-phases.md quebra o trabalho em fases numeradas, cada uma com tasks, acceptance criteria verificáveis e rastreio de volta para a spec:
Phase 1: Fundação — Laravel AI SDK, ausência de banco e base de UI
Phase 2: Agente CronExplainer — instruções e contrato de saída
Phase 3: Interface Livewire e fluxos completos
Phase 4: Gate de qualidade e fechamento do MVP
Cada critério é objetivo o bastante para ser conferido sem julgamento — laravel/ai aparece em require e não em require-dev, artisan about não reporta erro de provider, e assim por diante. Critério vago não sobrevive a um loop.
O espelho executável fica em .phases/, com manifest.txt (fase → arquivo → título, carimbado com o sha do plano de origem) e .progress (o que já passou).
loop.php — ~90 linhas, sem dependência nenhuma — é o motor. Para cada fase:
- Implementa. Chama
claude -pcom o texto integral da fase e a instrução de implementá-la completamente, sem TODOs e sem placeholders. - Testa. Roda
./vendor/bin/sail pest. Se falhar, as últimas 30 linhas da saída viram o motivo realimentado na próxima tentativa. - Verifica. Chama um segundo agente, independente e read-only (
--allowedTools "Read,Glob,Grep") que lê o código real e responde exatamente uma linha:DONEouFALTA — <o que não está implementado>. A instrução é explícita: na dúvida, FALTA. - Commita. Só com
DONE, e a fase vira um commit. - Insiste. Até
MAX_TRIES = 3, cada tentativa recebendo o motivo da reprovação anterior. Depois disso, desiste e segue — falha visível, não silenciosa.
$veridct = claude($prompt, '--allowedTools "Read,Glob,Grep"', true);
if (preg_match('/^DONE/m', $veridct)) {
exec('git add -A && git commit -q -m '.escapeshellarg("feat: $title"));
break;
}O ponto não é o script — é a separação de papéis. Quem escreve o código não decide se o código está pronto. O verificador não tem permissão de escrita, então não pode "consertar" o problema em vez de reportá-lo. E o gate mecânico (Pest) roda antes do gate de julgamento, porque é mais barato.
O histórico do repositório é a saída do loop, um commit por fase:
df9d3fc feat: Phase 1: Fundação — Laravel AI SDK, ausência de banco e base de UI
f00c6c1 feat: Phase 2: Agente CronExplainer — instruções e contrato de saída
dfd0525 feat: Phase 3: Interface Livewire e fluxos completos
4591165 feat: Phase 4: Gate de qualidade e fechamento do MVP
Descobertas caras viram regra escrita em .ai/rules/, não conhecimento perdido no fim da sessão. Exemplos reais deste projeto, todos achados durante a execução:
- SFC compila no namespace global.
use Throwable;dentro do bloco PHP de um single-file component dispara "The use statement with non-compound name has no effect", que o Laravel converte emErrorExceptione derruba a página com 500. Solução:catch (\Throwable $e)qualificado. (.ai/rules/pages.md) - Rodar
pintapontando direto para o.blade.phpaplicafully_qualified_strict_typese reescreve exatamente as barras invertidas que a regra acima exige. Rode sempre sem argumento de path. - O stub de
make:agent --structuredgera a classe implementando tambémConversationaleHasTools— os dois foram removidos, porque o agente é stateless. (.ai/rules/ai.md)
Cada uma dessas é uma armadilha que custaria uma iteração inteira do loop na próxima vez. Escrita como regra com glob, ela é lida antes de o próximo agente tocar naquele caminho.
O repositório carrega o próprio manual de operação:
AGENTS.md/CLAUDE.md— contexto canônico do projeto, gerado a partir do código implementado e regerável quando ele muda;.ai/rules/— regras por glob, comindex.mdmapeando caminho → arquivo de regra;.mcp.json— o servidor Laravel Boost, dando ao agentesearch-docs,database-schema,browser-logse afins em vez de chutes;.codex/,.grok/,.agents/— o mesmo contexto exposto para outros clientes de agente.
descrição → spec → fases com critérios binários → loop (implementa → testa → verifica → commita) → regras
↑ │
└──────────── o que se aprendeu ───────────┘
Onde o humano gasta tempo: escrevendo a spec, apertando os critérios de aceite e endurecendo o gate. Não revisando diff linha a linha — se o diff está errado com frequência, o defeito está na spec ou no gate, e é lá que se corrige.