Skip to content

dougladmo/graph-eng

v1.0.0

Planeja e executa tarefas complexas como grafos pequenos com checks verificáveis e evidências locais.


Em 30 segundos

Você descreve a tarefa. Um modelo forte planeja e revisa, modelos baratos executam, e o supervisor roda testes de verdade em cada passo. A run só termina como DONE quando há evidência; senão, para com um motivo legível.

🧠 Planeja forteO modelo forte do host monta o plano como grafo e revisa cada etapa.
⚙️ Executa baratoO modelo operacional do host, mais barato, faz o trabalho de cada nó.
🧪 Confere com testesnode --test nos arquivos de cada nó e a suíte inteira no fim.
🔁 Corrige pelo caminhoO feedback dos revisores volta pelo grafo: refaz um nó, replaneja ou para.
🔒 Sem DONE falsoSem testes aprovados, critic aprovado e evidência atual, o DONE é negado.

Versão atual: 1.0.0 · veja o CHANGELOG.

Instalação

Numa sessão do Claude Code, um comando de cada vez:

/plugin marketplace add Dougladmo/graph-eng
/plugin install graph-eng@graph-eng

Pelo terminal:

claude plugin marketplace add Dougladmo/graph-eng
claude plugin install graph-eng@graph-eng

Requisitos: node 22 no PATH. O pacote não tem dependência de runtime.

Os cinco comandos

O caminho normal é de cima para baixo. Cada passo reaproveita o anterior enquanto ele ainda vale.

flowchart TD
  I([init<br/>sobe o painel]) -.-> P
  P[plan<br/>gera o plano] --> Q{tem perguntas?}
  Q -- sim --> C[clarify<br/>responde as perguntas] --> T
  Q -- não --> T[tasks<br/>quebra em tarefas]
  T --> M[implemente<br/>executa e testa]
  M --> R([DONE ou BLOCK<br/>com motivo])
ComandoO que fazMexe no código?
plan <tarefa>Gera o plano. Se faltar informação, termina com needs_clarification.❌
clarifyResponde as perguntas pendentes do plano. Não inventa respostas.❌
tasksQuebra o plano aprovado em tarefas (um grafo de dependências).❌
implemente [<tarefa>]Executa as tarefas, roda os testes e decide DONE ou BLOCK.✅
initSobe (ou reaproveita) o painel local e mostra a porta.❌
HostComo chamar
Claude Code/graph-eng:plan <tarefa>, /graph-eng:clarify, /graph-eng:tasks, /graph-eng:implemente [<tarefa>], /graph-eng:init (ou /graph-eng:grapheng <comando>)
Codexa skill graph-eng:grapheng, com os mesmos cinco comandos. Não há slash global do plugin no Codex

Há um slot de plano e um de tarefas por repositório, a feature corrente: um plan aprovado novo substitui o anterior. Se algo mudou desde o passo anterior, o runner recusa o reuso e diz o motivo (workspace_changed, task_changed, tasks_plan_mismatch, source_unverified, entre outros).

Como funciona

O ciclo inteiro

flowchart TD
  P[🧠 Planner<br/>monta o plano] --> R{🧠 Revisor do plano}
  R -- pede ajuste --> P
  R -- aprova --> W[⚙️ Executores<br/>um por nó do grafo]
  W --> C[🧪 Supervisor roda os testes do nó]
  C --> V{🔍 Verificador do nó}
  V -- FIX_LOCAL --> W
  V -- aprovado --> PO[✨ Polish por área]
  PO --> G[🧪 Gate final<br/>suíte inteira]
  G --> K{🧠 Critic}
  K -- FIX_LOCAL --> W
  K -- REPLAN, no máximo 2 --> P
  K -- BLOCK --> B([⛔ BLOCK com motivo])
  K -- DONE com evidência --> D([✅ DONE])

Quem faz o quê

Os modelos de cada papel são fixos por host (Claude Code ou Codex): forte para planejar e revisar, operacional para executar. Não há flag para trocar.

PapelTipoO que faz
Planner🧠 forteMonta o plano como grafo, com critérios de pronto (doneWhen).
Revisores (plano e design)🧠 forteAprovam ou devolvem com o motivo.
Critic🧠 forteConfere cada critério de pronto contra a evidência e decide.
Executor⚙️ baratoFaz o trabalho de um nó, só nos arquivos liberados para ele.
Verificador⚙️ baratoLê o que o executor fez e os recibos dos testes.
Synth⚙️ baratoEscreve o resumo final.
Supervisor🧪 códigoRoda os testes, sela a evidência e nega DONE sem prova. Não é um modelo.

As quatro decisões

Todo revisor responde com uma destas, sempre com evidência:

DecisãoO que aconteceLimite
🔧 FIX_LOCALRefaz só o nó afetado, com o motivo do revisor no prompt.conta no orçamento do nó
🗺️ REPLANGera um plano novo. Os critérios de pronto do usuário ficam travados.no máximo 2 por run
⛔ BLOCKPara e mostra o motivo (por exemplo original_plan_criteria_changed).—
✅ DONEFecha a run. Exige testes aprovados, critic aprovado e evidência atual.negado sem prova

Como um nó vira pronto

flowchart TD
  E[⚙️ Executor altera só os arquivos liberados] --> D{🧪 Supervisor: o diff ficou<br/>dentro dos arquivos liberados?}
  D -- não --> X([⛔ BLOCK])
  D -- sim --> T[🧪 Supervisor roda node --test<br/>dos arquivos do nó]
  T --> V{🔍 Verificador lê o diff<br/>e os recibos dos testes}
  V -- FIX_LOCAL --> E
  V -- aprova --> S([🔒 Supervisor sela a evidência do nó])

Regras que valem sempre:

  • O nó só mexe nos arquivos que o plano liberou para ele. Arquivo fora disso bloqueia.
  • A suíte inteira roda uma vez, no gate final, depois de todo polish.
  • A retomada nunca devolve orçamento, e os contadores nunca diminuem.

O que mudou da 0.1 para a 1.0

0.11.0
Fluxografo de ida, com critic no fimgrafo bidirecional: o feedback volta e muda o plano
Fim da runrelatório PARTIAL, sem saber se terminouDONE com evidência, ou BLOCK com motivo
Testescomando livre por nónode --test selecionado pelos arquivos do nó, suíte inteira no fim
Configuraçãoesforço × teto de agentes, mais vários ajustes finossó --effort low|medium|max
HostsClaudeClaude e Codex
Painelvisualização e fila de açõesvisualização ao vivo, só leitura

Benchmark

bin/graph-benchmark.mjs roda uma tarefa fixa numa cópia limpa do repositório, com Claude real. Quem dá a nota é um conjunto de testes de aceitação externos, que os agentes não veem. Detalhe de todas as rodadas em docs/validation/graph-eng-2-benchmark.md.

Rodada f366:

graph-eng 1.0
💵 Custo da rodadaUS$ 0,80
✅ Testes externos10/10
🏁 O que o motor disseDONE com evidência
🔒 DONE falsonão
📞 Chamadas de modelo21
🔤 Tokens (entrada / saída)605k / 36k
⏱️ Tempo do motor292 s

Onde vai o dinheiro:

PapelModeloChamadasUS$
🧠 criticforte2██████ 0,31
🧠 plannerforte2████ 0,18
🧠 revisor do planoforte2███ 0,17
🧠 revisor do designforte1██ 0,10
⚙️ executoroperacional80,015
🔍 verificadoroperacional50,011
⚙️ synthoperacional10,005
Total210,80

O modelo forte responde por cerca de 95% do custo. As 14 chamadas do modelo operacional somam só 3 centavos. Para baixar o custo, o lugar é nas chamadas fortes (critic, planner e revisores), e esse trabalho segue em aberto.

  • 💰 O isolamento do host derrubou o custo de cerca de US$ 88 para menos de US$ 1 por rodada: o host deixou de vazar cerca de 75k tokens de contexto por chamada e agora manda cerca de 6,5k.
  • 📈 Em 13 rodadas reais, a 1.0 nunca deu DONE falso. Cada BLOCK virou correção com teste de regressão, até fechar DONE na f366.
  • 🧪 O cenário ainda é simples. Falta um cenário mais difícil.
  • ⚠️ O host Codex real ainda não foi validado.

Detalhes de execução

O esforço aceita low, medium (padrão) ou max, na forma separada (--effort max); --effort substitui a configuração em plan, clarify, tasks e implemente. Papéis e modelos são fixos pelo adapter confiável de Claude ou Codex. Não há flags públicas para modelos, papéis, largura ou limites internos.

O adapter de execução implementado neste corte é somente Node native test runner em repositórios com package.json legível e scripts.test exatamente node --test. Vitest, Jest, scripts com flags extras e repositórios sem esse contrato produzem BLOCK para implemente; plan continua disponível. Cada nó recebe uma seleção local derivada de inputs, outputs e files aprovados pelo supervisor. Um mapping interno conhecido roda os testes afetados; sem mapping completo, a cobertura é marcada como limitada e só se executam node --check aplicáveis e a validação do contrato/grants. O gate final roda a suíte completa com concorrência 2 somente depois de todo polish. Os checks usam process.execPath, argv explícito e spawn sem shell. Cada run grava seleção e receipts em .graph-runs/ no repositório.

O runner informa o estado e os bloqueios encontrados durante a execução. Quando não há mapeamento confiável de checks, implemente fica bloqueado; não invente comandos para contornar isso.

Concorrência no mesmo repositório

Enquanto um implemente roda no mesmo repositório, plan, clarify e tasks podem ser bloqueados com workspace_busy. Isso é esperado: o lock do workspace é o mesmo para leitura e escrita, e só sai quando a run termina.

CLI direto (bin/graph-run.mjs)

O uso direto do runner é para depuração. node bin/graph-run.mjs --host <claude|codex> --args <arquivo.json> bloqueia implementação sem nodeCheckPolicy com write_audit_unavailable, sem criar host nem runDir, e sai com código 2. Plano só de leitura (planOnly, research, architecture, review) passa sem política.

Retomar pelo CLI direto exige repetir mode e nodeCheckPolicy nos args. Um arquivo só com { "runId": "...", "resume": { ... } } cai no mesmo bloqueio, mesmo que a run original tenha política.

Limites de integridade da runDir

A run grava o estado em .graph-runs/<runId>/ e confere a árvore com um guard de integridade. Esse guard tem dois limites conhecidos:

  • A violação não persiste depois que o processo sai. Enquanto o processo que detectou a alteração está vivo, a run fica bloqueada com run_integrity_violation e não escreve REPORT. Quando a run é retomada depois, o baseline é recalculado a partir dos bytes atuais do disco, e o resume aceita o que estiver lá.
  • Durante o handle de um host, a saída de outro handle não é vigiada. Um agente com escrita na runDir pode trocar o .host-*.output.json de outro handle sem que o guard perceba, porque a saída não tem hash. O schema tem hash e é conferido na limpeza.

Um erro de checagem que não é violação, como um diretório ilegível no inventário, bloqueia com integrity_check_failed, mesmo que a permissão volte antes do fim da run, e não grava REPORT.

Custo: a checagem da árvore inteira roda por agente e custa O(bytes da runDir). Com runDir de 1 a 10 MB, cada checagem leva de 10 a 25 ms; acima de 100 MB, esse custo deve ser reavaliado.

Lock de workspace com dono morto

Cada workspace tem um lock de recurso no diretório de estado. Se a run encontra esse lock com dono morto, ela não o remove sozinha: a execução para com workspace_lock_owner_dead, e o detalhe traz o caminho do arquivo e o pid do dono. Um pid vivo com outra identidade de processo (pid reutilizado) conta como dono morto.

Remoção manual: confirme que o pid do detalhe não existe mais e só então apague o arquivo indicado. Um lock com JSON inválido (workspace_lock_invalid) ou com dono ambíguo (workspace_lock_ambiguous) também fica parado para inspeção e exige a mesma conferência antes de qualquer remoção.

Memória entre runs

Ao fim de cada run sem violação de integridade e com lease confiável (sem falha de heartbeat), o supervisor registra na memória do repositório (~/.claude/graph-eng/memory/<repoId>, fora da runDir) os registros de design aprovado e de checks válidos. Falha de memória ou de registro vira diagnóstico no journal e nunca muda o status da run.

Uma run seguinte no mesmo repositório seleciona esses registros por nó e contrato. Cada worker recebe só os registros do próprio nó e com a mesma identidade de contrato pré-trabalho, num bloco <memoryContext> com o aviso de que o conteúdo são dados não confiáveis. Os registros vêm como JSON, com < escapado, então um resumo não consegue fechar a tag nem virar instrução. O evento memory.selected aparece no journal antes do agente.

Na retomada, a seleção fixada da run é reaproveitada só depois de conferida. O evento memory.selected sai de novo com reused: true, e o painel mostra essa marca. Se a conferência falhar, a run registra um diagnóstico selection_reuse_rejected e faz uma nova seleção.

Limitações desta versão (v1.0):

  • O planner não recebe seleção de memória. Ele não tem nó nem caminho definidos no início da run, e a seleção exige um escopo que case com algum registro.
  • O critic não recebe seleção de memória. O ramo que selecionava por caminho não tinha produtor de registro com caminhos; foi retirado e não é relaxado. Registros de memória não entram na revisão crítica.
  • Evidência da fase final fica fora. Receipts de checks finais (depois do polish) não têm escopo por nó e não alimentam a memória. Só os receipts da fase de nó entram, com escopo nodeId + contractIdentity, e só o worker do mesmo nó e contrato os recebe.
  • Contract, failure e version ficam fora da v1.0. Esses tipos existem no modelo de registro, mas nenhum produtor os grava e nenhum papel os seleciona. Nada é alimentado sem consumidor.

Estado final no painel

O evento run.finished é o último do journal e carrega o status terminal. Ele sai depois do REPORT e antes da liberação do lease. Uma run planOnly bem-sucedida grava plan_only (plano concluído), não blocked.

Divergência conhecida: se a liberação do lease ou do workspace falha depois do run.finished, o journal continua com o status do resultado (por exemplo done), enquanto o retorno do CLI sai blocked com lease_release_failed ou workspace_release_failed. O painel só mostra o status do journal quando o lease já foi solto; com o lease preso, o status fica desconhecido.

Desenvolvimento

O pacote não precisa de instalação global. Com Node.js e dependências de desenvolvimento já instaladas, execute os testes do driver e dos manifests:

node --test --test-concurrency=2 test/grapheng.test.mjs test/packaging.test.mjs

O projeto usa node --test; alterações de UI podem exigir npm run css:build. A justificativa histórica do desenho está em DESIGN.md.

Licença

MIT

Star History