A VERDADE de um run mora no disco — o run-dir com events.jsonl append-only. Esta lição é sobre as LENTES que olham para ela: o TUI ASCII (alembic tui <run-id>, render puro + poll de 500ms), o cockpit web read-only sobre os run-dirs (com UMA escrita permitida: aprovações T4, append-only), e o servidor stateful do harness (alembic serve — HTTP+SSE, POST /runs inicia um run Forge de verdade). Três clientes, uma fonte.
Nenhuma das três superfícies inventa estado. O TUI lê index.json, events.jsonl e t4-parked.jsonl direto do run-dir a cada redraw (packages/tui/src/index.ts:92–94); o cockpit se descreve como "HTTP + SSE read-only view over run directories" (packages/web/src/server.ts:20–25); e até o servidor stateful do harness anota no próprio código que "the durable run directory holds truth" (apps/cli/src/commands.ts:1856–1857). Observabilidade aqui é literalmente: ler os mesmos arquivos que o replay lê.
Pense como… a caixa-preta de um avião com três telas na sala de controle: nenhuma tela "sabe" nada — todas projetam a MESMA fita. Onde a analogia quebra: aqui uma das telas tem um botão físico (aprovar T4) — e ele não edita a fita, só cola um post-it datado nela.
@alembic/tui separa o que é testável do que é terminal: TODA a renderização é pura — "No I/O here: these functions take structured state and return lines" (packages/tui/src/render.ts:1–6). renderRunFrame desenha uma caixa fixa de BOX_WIDTH = 80 colunas (render.ts:25) com header do run, stats (events: N | parked: N) e os eventos mais recentes como swimlanes (render.ts:95–120); a lane de um evento com taskId vira task:<id> (render.ts:61–65); o rodapé é literal: 'Press q to quit' (render.ts:118).
O loop interativo (packages/tui/src/index.ts:68–139) redesenha a cada refreshMs default de 500ms (index.ts:70), lendo os três arquivos do run-dir com parsers que NUNCA lançam — linha JSON quebrada vira evento { kind: 'raw' } (index.ts:34–51). q, Q ou Ctrl+C encerram limpo (index.ts:107–113), e o setRawMode é guardado por try para streams não-TTY (index.ts:115–121) — dá para injetar stdin/stdout fakes e testar o loop inteiro sem terminal (index.ts:16–23). Na CLI, runTui primeiro valida que o run EXISTE via readRunIndex — id errado é err antes de abrir tela (apps/cli/src/commands.ts:1906–1918).
const redraw = async (): Promise<void> => { const index = await safeReadJson(join(runDir, 'index.json')); const events = await safeReadJsonl(join(runDir, 'events.jsonl')); const parked = await safeReadJsonl(join(runDir, 't4-parked.jsonl')); const status = { runId: index?.runId ?? 'unknown', status: index?.status ?? 'unknown', goal: index?.goal, eventCount: events.length, parkedCount: parked.length }; const frame = renderRunFrame(status, events); // PURO — testável sem terminal clearAndHideCursor(stdout); for (const line of frame) stdout.write(`${line}\n`); };
alembic cockpit (commands.ts:1789–1806) sobe createCockpitServer (server.ts:156+) sobre --data-dir (default ~/.alembic, porta efêmera por default — apps/cli/src/args.ts:239–243). O dashboard em / lista runs e abre um EventSource para o stream (server.ts:339); a API: GET /api/runs (server.ts:204), GET /api/runs/:id/status e GET /api/runs/:id/events (consumidos pelo próprio dashboard — server.ts:442, :454), e GET /api/marketing (server.ts:209) — a visão read-only dos manifests da fábrica da lição 23: <dataDir>/marketing/<signalId>/manifest.json, validados por schema, "nothing is ever written" (packages/web/src/marketing.ts:7–16).
O stream SSE não usa watcher mágico: pollLines re-lê o events.jsonl append-only a cada 300ms e emite só as linhas novas (server.ts:124–150); cada frame carrega id (o seq do evento), event (o kind) e o JSON em data: (encodeSseFrame, server.ts:98–105). E a exceção que confirma a regra read-only: POST /api/runs/:id/approve APPENDA um registro em approvals.jsonl (server.ts:260–291) — o gesto humano do T4-park, gravado como ledger, nunca como edição.
server.ts:153–155, :260–291). Ele nunca muda status (isso é do motor que executa) nem compacta o journal (o SSE re-lê incrementalmente via pollLines, :124–150).O cockpit observa runs que já existem; alembic serve (commands.ts:1816–1901) é outra categoria: um servidor STATEFUL do harness cujo contrato REST vive em @alembic/harness — POST /runs, GET /runs/:runId/status, GET /runs/:runId/events (packages/harness/src/http.ts:118–121; um cliente sem SSE recebe o snapshot bufferizado dos eventos como fallback — http.ts:152–154).
O createRun injetado exige phase === 'run' COM goal e plan — senão err imediato (commands.ts:1836–1837). Com eles: loadScope materializa o escopo Forge no run-dir, um HarnessCore entra no registry (commands.ts:1848–1854) e o run parte em BACKGROUND via runGoalPlan enquanto a resposta volta imediatamente como snapshot 202 (commands.ts:1808–1815, :1858–1882) — você faz o POST, guarda o runId e passa a observar por status/SSE. Com --offline, o registry de adapters sobe com o offline como default — o servidor inteiro roda hermético (commands.ts:1828–1830).
| Superfície | Natureza | Fonte de estado | Escreve? |
|---|---|---|---|
alembic tui | terminal, poll 500ms | run-dir (3 arquivos) | nunca |
alembic cockpit | HTTP+SSE read-only | run-dirs + marketing/ | só approvals.jsonl (append) |
alembic serve | HTTP+SSE stateful | HarnessCore + run-dir durável | inicia runs (POST /runs) |
POST /runs chega ao alembic serve com { phase: 'run', goal: 'GOAL.md' } — sem plan. O que volta?if (body.phase !== 'run' || !body.goal || !body.plan) return err('POST /runs requires phase="run" with goal and plan paths') (commands.ts:1836–1837). Nada de plano derivado, nada de meia-execução: o contrato falha fechado antes de tocar o disco.A sequência do operador, toda read-only exceto o passo 1 (que só inicia se VOCÊ mandar):
| # | Comando | O que você vê |
|---|---|---|
| 1 | alembic run --goal GOAL.md --plan alembic.plan.ts --yes | o run parte; o run-dir nasce em ~/.alembic/runs/<id> |
| 2 | alembic runs list | ids + status de todos os runs no data-dir |
| 3 | alembic tui <run-id> | o frame ASCII de 80 colunas se redesenhando a cada 500ms; q sai |
| 4 | alembic tail <run-id> -f | o events.jsonl cru, streaming — o mesmo journal que o replay usa |
| 5 | alembic cockpit | dashboard web; runs, status, SSE ao vivo, e o botão de aprovar T4 |
Observabilidade é read-only por natureza — experimente à vontade:
# o render puro e o loop do TUI sed -n '1,10p;95,120p' packages/tui/src/render.ts sed -n '68,113p' packages/tui/src/index.ts # abrir o TUI num run existente (q sai; id errado = err antes da tela) alembic runs list alembic tui <run-id> # o cockpit e suas rotas (porta efêmera impressa no start) alembic cockpit curl -s http://localhost:<porta>/api/runs | head -c 400 curl -N http://localhost:<porta>/api/runs/<run-id>/events # SSE ao vivo # o contrato REST do harness + o gate do POST /runs sed -n '116,124p' packages/harness/src/http.ts sed -n '1834,1840p' apps/cli/src/commands.ts # subir o serve hermético e iniciar um run via HTTP alembic serve --offline curl -s -X POST http://localhost:<porta>/runs \ -H 'content-type: application/json' \ -d '{"phase":"run","goal":"GOAL.md","plan":"alembic.plan.ts","yes":true}'
serve é o harness stateful: POST /runs exige phase+goal+plan, materializa o escopo Forge e roda em background com snapshot 202 (commands.ts:1836–1882; http.ts:118–121).--offline no serve troca o registry de adapters para o default hermético (commands.ts:1828–1830).@alembic/infra, DevOps como código em três planos, com provisionamento que NASCE dry-run.