Lição 27 · Fusão v3 · @alembic/loop-engineering ← índice
Alembic × Hermes — O Curso de Fusão v3 · Fechamento (26–30)

@alembic/loop-engineering: a disciplina vira pacote

A disciplina que dirigiu TODA a fusão — learn → analyze → execute UMA → verify → decide, com Proof Gate e done-when mensurável — não mora só na cabeça do operador: ela está internalizada no monorepo como um pacote com duas metades. skill/ embarca a doutrina inteira em markdown; src/ implementa um runtime mínimo tipado: escopo com doneWhen obrigatório, log append-only e um gate que só libera com prova.

2
metades: skill/ (doutrina em markdown) + src/ (runtime tipado)
5
fases no LoopLogEntry — learn·analyze·execute·verify·improve
8
testes em runtime.test.ts (prove: grep -c "it(" no arquivo)
0
scripts "test" no package.json — só a suíte raiz roda o pacote
1

O pacote que carrega a própria doutrina

packages/loop-engineering/src/index.ts quase não tem lógica — e isso é a tese. Ele exporta CAMINHOS: skillDir (index.ts:9) aponta para a árvore skill/ embarcada, e dali saem loopEngineeringSkillPath (o SKILL.md, index.ts:14), os dois scripts do Publish Gate (publish-course-gist.js em index.ts:19 e publish-course-pages.js) e os módulos da doutrina: forge/, fusion/, council/, visual-teach/, ultragoal/, brightdata-cli/, computer-use-cli/, harness-brief/, além de reference.md, USE-CASES.md, grill.md e setup.sh. O package.json reexporta tudo como "./skill/*" — qualquer agente instala o pacote e recebe a disciplina INTEIRA, legível, junto com o código.

Duas honestidades de mapa: (1) o Course Gate NÃO mora mais aqui — index.ts:42 reexporta generateCourse de @alembic/coda ("moved… re-export for compatibility"); (2) este pacote depende só de @alembic/coda, @alembic/contracts e @alembic/tui, e o package.json não declara script test — os 8 testes de runtime.test.ts rodam pelo glob do workspace raiz (guarde isso: é uma das gotchas da lição 29).

harness · 0005-loop-agente-guardrails · s0
skill/ — a DOUTRINA embarcada SKILL.md · forge-flow.md · grill.md forge/ fusion/ council/ visual-teach/ ultragoal/ brightdata-cli/ computer-use-cli/ harness-brief/ reference.md USE-CASES.md publish-course-gist.js · publish-course-pages.js exportada como "./skill/*" no package.json src/ — o RUNTIME mínimo tipado LoopScope { goal, doneWhen[] … } readScope / writeScope (md ⇄ json) appendLoopLog → LOOP-LOG.md (append-only) evaluateScopeGate (o Proof Gate do escopo) runLoopEngineering (executor INJETADO) 8 testes em runtime.test.ts (glob do raiz) index.ts:9–38 exporta os CAMINHOS da doutrina · index.ts:42 reexporta generateCourse de @alembic/coda (Course Gate mudou de casa)
Duas metades, um contrato: quem instala o pacote recebe a doutrina legível (skill/) e o runtime que a cobra (src/).

Pense como… um manual de voo encadernado junto com o checklist plastificado da cabine: o manual (skill/) explica o porquê; o checklist (src/) só deixa decolar se cada item for riscado. Onde quebra: checklist de avião não escreve diário — aqui cada passo vira linha permanente no LOOP-LOG.md.

2

O contrato: done-when mensurável ou nada

A primeira regra da disciplina — "capture o done-when ANTES de executar" — é código: scopeSchema (runtime.ts:25–52) exige goal string não-vazia e doneWhen array não-vazio; sem eles, err. readScope (runtime.ts:57–75) aceita duas formas: um .md com bloco cercado ```json sob o heading ## Scope (o formato que writeScope materializa), ou um .json puro. Um markdown SEM o bloco JSON é recusado com err('no JSON scope block found…') — prosa bonita não é escopo.

packages/loop-engineering/src/runtime.ts:25–40 (trecho real)
const scopeSchema = (raw: unknown): Result<LoopScope, Error> =>
  tryCatch(() => {
    if (raw === null || typeof raw !== 'object') {
      throw new Error('scope must be an object');
    }
    // …
    const goal = obj.goal;
    if (typeof goal !== 'string' || goal.length === 0) {
      throw new Error('scope.goal is required and must be a non-empty string');
    }
    const doneWhen = obj.doneWhen;
    if (!Array.isArray(doneWhen) || doneWhen.length === 0) {
      throw new Error('scope.doneWhen is required and must be a non-empty array');
    }
    // … context/constraints/editableSurface/agents opcionais
  });
O Proof Gate em uma função: evaluateScopeGate(scope, proofs) (runtime.ts:131–138) cruza cada item do doneWhen com a lista de provas e devolve { passed, missing }. Só passa o item cuja prova é literalmente true; o resto aparece NOMEADO em missing. "Deve funcionar" não tem representação neste tipo.
3

O loop de verdade: uma unidade, uma prova, uma linha no log

runLoopEngineering (runtime.ts:178–238) é o control plane: lê o escopo, e para CADA LoopUnit grava uma entrada execute no log, chama o executor INJETADO, verifica com o verifier (ou o próprio executor como fallback — verified = succeeded && verify(unit)), empilha a prova, grava a entrada verify (converged ou iterate) e segue. No fim, evaluateScopeGate decide a entrada final improve: converged ("scope gate cleared") ou blocked com os itens faltantes por nome. O log é escrito com flag: 'a' (appendLoopLog, runtime.ts:116–123) — append-only: a história dos passos não se reescreve.

Repare na honestidade do vocabulário: o tipo LoopLogEntry.phase (runtime.ts:101) admite learn | analyze | execute | verify | improve — mas o runtime só GRAVA execute, verify e improve. As fases learn/analyze são do agente que dirige (ele pode registrá-las); o pacote não finge pensar por você. E o executor ser injetado é a separação builder/validador da casa: quem executa a unidade e quem a verifica podem ser processos diferentes.

launch-academy · 07-practice-labs · s0
LoopUnit id · proves executor(unit) INJETADO pelo caller verifier ?? executor verified = ok E ok proofs[] item: proves ?? id próxima unidade (UMA por vez) evaluateScopeGate todo doneWhen provado? improve · converged 'scope gate cleared' improve · blocked 'scope gate blocked: <itens faltantes>' LOOP-LOG.md — flag:'a', append-only execute → verify → … → improve (nada se reescreve)
runLoopEngineering (runtime.ts:178–238): executor injetado, prova por unidade, gate no fim — e cada passo vira linha permanente.
honestidade de vocabulárioO runtime NÃO pensa por você: learn e analyze existem no tipo para o AGENTE registrar as suas fases; o pacote só grava o que consegue provar sozinho — execute, verify e improve.
O que o LOOP-LOG.md responde depois: qual unidade rodou quando e com que prova; onde o gate bloqueou e por quê; e — por ser append-only — o que foi TENTADO antes de funcionar. É exatamente a matéria-prima que o failure-historian digere na lição 30.
Um escopo .md caprichado descreve o objetivo em prosa, sem bloco ```json sob ## Scope. O que readScope faz?
Correto: readScope (runtime.ts:57–75) só reconhece o bloco cercado ```json (ou um arquivo JSON puro) e falha fechado sem ele. E mesmo com JSON, scopeSchema ainda exige goal não-vazio e doneWhen não-vazio — o done-when mensurável é pré-condição, não enfeite.
Uma unidade executa com sucesso, mas o verifier injetado devolve false. Qual é o efeito no relatório final?
Correto: verified = succeeded && verify(unit) — a prova empilhada é false, unitsPassed não incrementa, e evaluateScopeGate lista o item em missing, produzindo a entrada final improve · blocked: <item>. Não há retry escondido: iterar é decisão do agente que dirige, com o log como memória.
Camada técnica — comandos copy-paste

Verifique a lição inteira com leitura pura ($0):

# as duas metades: doutrina embarcada + runtime
sed -n '1,45p' packages/loop-engineering/src/index.ts
ls packages/loop-engineering/skill/ | head -20

# o contrato do escopo e o Proof Gate
sed -n '25,75p' packages/loop-engineering/src/runtime.ts
sed -n '131,138p' packages/loop-engineering/src/runtime.ts

# o loop por unidade e a entrada final improve
sed -n '178,241p' packages/loop-engineering/src/runtime.ts

# o append-only literal (flag 'a') e a doutrina exportada como pacote
grep -n "flag: 'a'" packages/loop-engineering/src/runtime.ts
grep -n '"./skill/' packages/loop-engineering/package.json | head -5

# os 8 testes reais (e onde eles rodam: glob do raiz, não script local)
grep -n "it(" packages/loop-engineering/src/runtime.test.ts
grep -n '"test"' packages/loop-engineering/package.json   # (vazio — ponte p/ lição 29)
O que levar desta lição
Pergunta de acompanhamento sugerida: "se o runtime não executa nada sozinho, quem o chama na prática?" — os callers de hoje são os agentes que dirigem runs (via CLI/harness); repare como isso rima com o executor injetado. Próxima lição: a superfície onde TUDO isso vira comando de terminal — apps/cli, seus 33 comandos e a doutrina dry-run.