Tynnaretrato de 22/09, 21:52
voltar à Biblioteca

Visão de CEO — design

docs/superpowers/specs/2026-09-22-visao-de-ceo-design.md

Visão de CEO — design

Data: 2026-09-22 · Origem: brainstorm de 21/09/2026 (artefato visual: claude.ai/code/artifact/f9aa867d) · Status: aprovado pelo fundador em 22/09, aguardando revisão do texto.

1. O que é

Uma área nova em tynna.app/empresa onde cada área da empresa aparece com um estado (verde, amarelo, vermelho, desconhecido), o fundador desce só onde algo está fora do padrão e decide o que é dele (aprovar, vetar, escalar) sem sair da tela. Substitui o dashboard atual (/cockpit, /, /m/triage) que lê as fontes ao vivo em cada render e por isso ninguém abre.

Critério de sucesso, nas palavras do fundador: "abro todo dia e sei em um minuto onde olhar, sem perguntar ao agente".

Decisões tomadas no brainstorm

PerguntaDecisão
PropósitoSaúde por área e mergulhar no que está fora do padrão. Não é feed de atividade.
Áreas na v1Só o que tem fluxo real hoje (Desenvolvimento). A estrutura cresce por contrato conforme áreas nascem.
O que o humano fazVer e decidir: aprovar, vetar, escalar. Nunca editar código, prompt, gate ou contrato pela tela.
Ondetynna.app, substituindo o dashboard atual.
AbordagemEstado materializado por contrato: um coletor agendado grava retratos numa tabela; a tela só lê a tabela.

Por que "estado materializado" e não "ler ao vivo" nem "eventos"

  • Ler ao vivo (o que /cockpit faz hoje) acopla a tela a quatro APIs, fica lento, e cada abertura da página custa chamadas. Sem histórico, não há tendência.
  • Eventos (webhooks GitHub/Paperclip → fila → projeção) é o desenho certo para escala, mas exige receptor sempre no ar, validação de assinatura e reprocessamento. Para uma área instrumentada, é peso sem retorno.
  • Retrato a cada 15 min dá histórico de graça, um único ponto de falha vigiado pelo dead-man switch, e pode virar eventos depois sem mudar a tela (a tela só conhece a tabela).

2. Arquitetura

Fontes ──▶ coletor-de-estado ──▶ retratos_de_area ──▶ tynna.app/empresa
(GitHub,     (workflow agendado,      (Supabase)            │
 Paperclip,   */15, declarado no                            ▼
 agent_runs,  dead-man switch)                        decisoes_tomadas ──▶ GitHub / Paperclip
 vigia/host)        ▲                                  (trilha)             (label, status, comentário)
                    │
             config/sinais.yml (contrato por área)

Quatro peças, uma seta só. A tela nunca fala com as fontes.

PeçaResponsabilidadeDepende de
config/sinais.ymlDeclarar, por área: fontes, sinais, limiares e decisões do fundador.nada
scripts/estado/ (coletor)Ler o contrato, consultar fontes, avaliar limiares, gravar um retrato por área.contrato, credenciais de leitura, Supabase
retratos_de_area, decisoes_tomadasGuardar retratos e trilha de decisões.migration em apps/web/supabase/migrations/
apps/web/app/(dashboard)/empresa/Ler a última linha por área, mostrar, e aplicar decisões na fonte gravando na trilha.tabelas, sessão do usuário

O coletor é um gatilho agendado no mesmo padrão que substituiu a frota (ADR-0083): sem sessão viva, sem credencial própria além das da esteira, declarado em config/mecanismos-agendados.yaml para que o dead-man switch alarme se parar. É o "twin de plantão" da extensão 15 §7.1, materializado numa tabela.

3. O contrato de sinais — config/sinais.yml

Instrumentar uma área é declarar isto. Não é construir tela. Mesma filosofia do config/escopos.yml.

# config/sinais.yml — cada área declara o que é saúde e o que é decisão do fundador
versao: 1
coleta:
  cadencia_minutos: 15
  retrato_velho_apos_minutos: 30    # a tela destaca "retrato de há N min" a partir daqui

areas:
  desenvolvimento:
    nome: "Desenvolvimento"
    sinais:
      fila_de_merge:
        fonte: github.prs_abertos          # lista de PRs abertos com idade
        amarelo:  "algum.idade_horas > 24"
        vermelho: "algum.idade_horas > 72 or total > 8"
      gates_no_main:
        fonte: github.checks_main          # conclusão dos required no head de main
        vermelho: "algum.conclusao != 'success'"
      custo_agentic_semana:
        fonte: supabase.agent_runs         # soma de custo dos últimos 7 dias
        amarelo:  "valor > teto * 0.8"
        vermelho: "valor > teto"
        parametros: { teto_usd: 60 }
      host_ci:
        fonte: github.runners              # runners online/ocupados no host
        amarelo:  "ocupados >= total"
      migrations_orfas:
        fonte: config.migrations_sem_pipeline
        amarelo:  "total > 3"
    decisoes:
      - id: ratificar_adr
        fonte: github.prs_com_label
        parametros: { label: "adr" }
        acoes: [aprovar, vetar]
      - id: aprovacao_l3
        fonte: github.prs_tier_high_vetados
        acoes: [aprovar, vetar, escalar]
      - id: decisao_de_board
        fonte: paperclip.cards_com_tag
        parametros: { tag: "decisao-board" }
        acoes: [aprovar, vetar, escalar]
      - id: decisao_do_motor
        fonte: supabase.tynna_decisions_pendentes
        acoes: [aprovar, vetar]

  sustentacao:
    nome: "Sustentação"
    sinais: {}          # declarada sem sinal: aparece cinza como lembrete
    decisoes: []

Regras do contrato (validadas ao carregar; violação = coletor recusa rodar)

  1. Toda fonte citada existe no registro de fontes do coletor (scripts/estado/fontes/). Fonte desconhecida é erro, não desconhecido.
  2. Toda expressão de limiar é parseável pela gramática abaixo. Erro de sintaxe é erro de carga, não sinal verde por acidente.
  3. Toda decisão declara acoes como subconjunto de [aprovar, vetar, escalar]. Não existe ação editar; o esquema a recusa.
  4. Área declarada com sinais: {} é permitida (aparece cinza). Área ausente do contrato não aparece na tela.
  5. Ordem de avaliação: vermelho, depois amarelo; nenhuma casando = verde. Sinal sem vermelho nem amarelo é erro de carga (sinal que nunca muda não é sinal).

Gramática de limiar

Expressão booleana sobre o valor da fonte. Subconjunto pequeno, avaliador próprio (sem eval):

  • Operandos: valor, total, campos numéricos do valor (ocupados), parametros.* (abreviado: teto), literais numéricos e strings entre aspas simples.
  • Quantificadores sobre listas: algum.<campo> <op> <literal>, todos.<campo> <op> <literal>.
  • Operadores: > >= < <= == !=, and, or, not, parênteses.
  • Resultado: true, false, ou desconhecido se qualquer operando não existe no valor (nunca coage ausência para 0).

4. Modelo de estado

retratos_de_area — uma linha por área por coleta

colunatiponota
iduuid pk
areatextchave do contrato
coletado_emtimestamptz
estadotextverde | amarelo | vermelho | desconhecido — o pior entre os sinais (vermelho > desconhecido > amarelo > verde)
estado_desdetimestamptzcalculado: coletado_em do retrato mais antigo da sequência contígua com o mesmo estado
sinaisjsonbobjeto com uma chave por sinal do contrato; cada valor é { valor: unknown, estado: Estado, limiar_casado: string | null, fonte: string, url: string | null }
decisoesjsonblista de pendentes nesta coleta; cada item é { id: string, ref: string, titulo: string, contexto: string, url: string, acoes: Acao[] }
fontes_falhastext[]fontes que não responderam nesta coleta

Índice (area, coletado_em desc). Retenção: 90 dias, apagados pelo próprio coletor ao fim de cada corrida (não é job separado, para não virar mecanismo agendado a mais).

Fora do padrão tem definição precisa, calculada na leitura: estado != 'verde' ou estado difere do retrato anterior da mesma área ou decisoes não vazio. É isso que sobe para o topo da tela.

decisoes_tomadas — trilha, escrita antes de tocar a fonte

colunatiponota
iduuid pk
area, decisao_idtextchave do contrato
reftextPR #53, TYN-3533, uuid de tynna_decisions
acaotextaprovar | vetar | escalar
motivotext nullobrigatório para vetar e escalar
poruuidauth.uid() de quem clicou
emtimestamptz
efeitojsonb{ tentado: { fonte: string, operacao: string, alvo: string }, ok: boolean | null, resposta: unknown, erro: string | null }ok=null enquanto a fonte não respondeu

Relação com tynna_decisions (já existe)

tynna_decisions é a fila de decisões pendentes do motor F1 (TTL 3 dias úteis, ADR-0075). Não é substituída: vira uma fonte (supabase.tynna_decisions_pendentes) e aparece em "Espera você". Aprovar/vetar por ali grava em decisoes_tomadas e atualiza o status na própria tynna_decisions. /m/triage deixa de existir quando /empresa cobrir as três origens que ele lia (Paperclip, PR, tynna_decisions).

RLS

Ambas as tabelas: RLS ativo. Leitura para authenticated com papel board (mesmo predicado usado por tynna_decisions). Escrita em retratos_de_area só por service_role (coletor). Escrita em decisoes_tomadas por authenticated board via server action, com por = auth.uid() forçado por policy with check.

5. O coletor — scripts/estado/

scripts/estado/
  coletar.ts            # entrada: carrega contrato, corre fontes, avalia, grava, poda
  contrato.ts           # parse + validação do sinais.yml (regras §3)
  avaliar-limiar.ts     # parser + avaliador da gramática (§3), puro
  retrato.ts            # monta a linha: pior estado, estado_desde, fontes_falhas
  fontes/
    index.ts            # registro: nome → função (parametros) → Promise<Valor | Falha>
    github.ts           # prs_abertos, checks_main, runners, prs_com_label, prs_tier_high_vetados
    paperclip.ts        # cards_com_tag
    supabase.ts         # agent_runs, tynna_decisions_pendentes
    config.ts           # migrations_sem_pipeline (lê o YAML do repo)

Fluxo de uma corrida:

  1. Carrega e valida o contrato. Inválido → exit 1 com a linha do erro; nada é gravado.
  2. Para cada área, para cada fonte distinta citada: chama uma vez (fontes são memoizadas por (nome, parametros) dentro da corrida). Falha → registra em fontes_falhas; todos os sinais dessa fonte ficam desconhecido.
  3. Avalia limiares. Expressão que resulta desconhecido → sinal desconhecido, log com a expressão.
  4. Monta o retrato, consulta o anterior para calcular estado_desde, insere.
  5. Apaga retratos com coletado_em < now() - 90d.
  6. Exit 0 mesmo com fontes falhas (o retrato foi gravado; o cinza na tela é o alarme). Exit 1 só para contrato inválido ou falha ao gravar.

Workflow .github/workflows/coletor-de-estado.yml: schedule: */15 * * * * + workflow_dispatch, concurrency: { group: coletor-de-estado, cancel-in-progress: false }, permissions: { contents: read, pull-requests: read, checks: read, actions: read }, runner tynna-novo. Entrada em config/mecanismos-agendados.yaml:

  - id: coletor-de-estado
    cadencia: "*/15"
    fonte: "workflow:coletor-de-estado.yml:*/15 * * * *"
    dono: twin
    efeito_se_nao_rodar: "tynna.app/empresa mostra retrato velho; fundador decide sobre dado morto"

Credenciais (todas de leitura, Keychain → GitHub Secrets, nunca no repo):

secretusoexiste hoje?
GITHUB_TOKEN (do workflow)PRs, checks, runnerssim
PAPERCLIP_API_KEY_LEITURAcardsnão — cunhar chave só-leitura no Paperclip
SUPABASE_SERVICE_ROLE_KEY_WEBgravar retratos, ler agent_runs/tynna_decisionsnão — copiar do projeto web

6. A tela — apps/web/app/(dashboard)/empresa/

Três níveis. Server components; a página lê apenas retratos_de_area (última linha por área + as 7 anteriores para a faixa) e decisoes_tomadas (para mostrar o que já foi decidido e não repetir).

Nível 1 — áreas. Uma linha por área do contrato: pastilha de estado, nome, "há N dias" (de estado_desde), "N esperam você". Ordenadas por fora-do-padrão primeiro. Cabeçalho: "retrato de há N min · próximo em M"; se N > retrato_velho_apos_minutos, o cabeçalho vira alerta.

Nível 2 — sinais da área selecionada. Um por linha: pastilha, nome, valor com unidade, faixa dos últimos 7 retratos. Clicar leva à origem (url do valor: lista de PRs, run, card). desconhecido é cinza com o nome da fonte que falhou; nunca verde.

Nível 3 — "Espera você". Um item por decisão pendente: título, contexto (extraído do PR/card pelo coletor: primeiras 400 chars do corpo ou do resumo do ADR), botões conforme acoes do contrato. vetar e escalar abrem campo de motivo obrigatório. Botão com loading + disabled no submit; server action idempotente (mesma (decisao_id, ref, acao) já na trilha com ok=true → não repete).

Rotas antigas: /cockpit e /m/triage passam a redirecionar para /empresa no mesmo PR que entrega o nível 3; lib/cockpit/* e lib/triage/* são removidos quando nada mais os importa (o execute-decision.ts do triage é reaproveitado para as ações Paperclip, com o repo corrigido para marckrs/tynna).

7. Como a decisão chega na fonte

decisãoação aprovaração vetaração escalar
ratificar_adrlabel ratificado no PR + comentário "ratificado por <login> em <data>"comentário com motivo; não fecha
aprovacao_l3label ciso-l2-aprovado no PRcomentário com motivocard no Paperclip com tag decisao-board, link do PR
decisao_de_boardcomentário com a opção + status in_progresscomentário + status cancelledstatus blocked + comentário
decisao_do_motortynna_decisions.status = 'approved'= 'rejected' + motivo

Identidade com que a ação é aplicada — requisito duro

O gate ciso-l2 lê o timeline do PR e só aceita label vinda de um User. Se o painel aplicasse como App ou com token de bot, o gate rejeitaria. Logo, toda ação em GitHub usa o token GitHub de quem está logado.

Hoje o login do tynna.app é e-mail+senha (Supabase). Isto adiciona:

  • Provedor GitHub no Supabase Auth do projeto web, escopo repo (o repo é privado), e a opção "Conectar GitHub" na página de login e no próprio /empresa.
  • O provider_token da sessão é usado só no servidor (server action) e nunca chega ao cliente. Ele expira em ~8h sem refresh; token ausente ou expirado → o botão de ação GitHub vira "Reconecte o GitHub", sem tentar fallback para credencial da esteira.
  • Ações no Paperclip e em tynna_decisions não têm essa restrição e usam a credencial do servidor (PAPERCLIP_BOARD_API_KEY, service_role), com por gravado na trilha.

Um teste estático garante que o cliente GitHub do painel recebe token só de session.provider_token e nunca de process.env.

Ordem de escrita

Toda ação: (1) insere em decisoes_tomadas com efeito.tentado e ok=null; (2) chama a fonte; (3) atualiza efeito com ok e resposta/erro. Se (2) falhar, a linha fica com ok=false e a tela mostra "não aplicado: <erro>". Auditoria não depende de a chamada ter funcionado.

8. Quando algo dá errado

falhacomportamento
Uma fonte não respondesinais dela desconhecido, fonte em fontes_falhas, retrato gravado. Tela: cinza com o nome da fonte. Nunca verde.
Coletor para de rodardead-man switch alarma (mecanismo declarado). Tela: cabeçalho em alerta após 30 min.
Contrato inválidocoletor recusa rodar, exit 1 com linha; último retrato bom permanece; dead-man não dispara (o workflow rodou), mas o job fica vermelho e a tela envelhece → alerta em 30 min.
Limiar não avaliável para o valorsinal desconhecido, log da expressão.
Botão falha na fontetrilha com ok=false e erro; tela "não aplicado: <motivo>". Não finge sucesso.
Token GitHub do usuário expiradobotão vira "Reconecte o GitHub"; nenhum fallback.
Duas coletas simultâneasconcurrency no workflow; a segunda espera.
Retrato duplicado na mesma coletaunique (area, coletado_em); insert idempotente.

9. O que fica travado por teste

invarianteonde
Contrato valida por esquema: fonte desconhecida, limiar mal formado, sinal sem limiar, ação fora de [aprovar, vetar, escalar] reprovamtests/standards/sinais-contrato.test.ts
Avaliador de limiar é puro, cobre cada operador, quantificadores, e desconhecido por operando ausente (nunca coage para 0)scripts/estado/avaliar-limiar.test.ts
Fonte falha ⇒ nenhum sinal dela é verdeprova de mutação: o teste injeta uma fonte que lança e afirma que trocar o desconhecido por verde no código reprovascripts/estado/coletar.test.ts
Pior estado: vermelho > desconhecido > amarelo > verdescripts/estado/retrato.test.ts
estado_desde reinicia na troca de estado e persiste na sequência contíguascripts/estado/retrato.test.ts
Coletor está em mecanismos-agendados.yaml com fonte: que casa o schedule do workflowjá coberto pelo gate do dead-man
Nenhum componente de app/(dashboard)/empresa importa cliente que escreva em código, prompt, gate ou contrato (lista negra: octokit.repos.createOrUpdateFileContents, git, fs.write*)tests/standards/painel-so-decide.test.ts
Toda ação grava na trilha antes de chamar a fonte (ordem observada por mock)apps/web/lib/empresa/__tests__/decidir.test.ts
Cliente GitHub do painel recebe token só de session.provider_token; process.env.GITHUB_TOKEN não aparece em lib/empresa/tests/standards/painel-token-do-usuario.test.ts
Server action idempotente: repetir (decisao_id, ref, acao) com ok=true não chama a fonte de novoapps/web/lib/empresa/__tests__/decidir.test.ts
RLS: authenticated sem papel board não lê; authenticated não escreve em retratos_de_area; porauth.uid() é recusadosuite pgTAP em apps/web/supabase/tests/
E2E happy-path: abre /empresa, vê área amarela, abre sinal, veta uma decisão com motivo, vê "vetado"Playwright, mesmo PR do nível 3

10. Pré-requisitos que não são deste desenho

Descobertos ao conferir o repo; sem eles a v1 não liga, e por isso entram como Fatia 0 do plano, não como surpresa no meio.

  1. db-push para apps/web. Hoje .github/workflows/db-push.yml enumera apps/*/supabase/migrations mas para no "estado 3 — ainda NÃO está implementado (Passo 34)". Criar retratos_de_area sem isso seria a 4ª e 5ª migration órfã (config/migrations-sem-pipeline.yml). O plano implementa o supabase db push para o projeto web antes de qualquer migration nova.
  2. Dois secrets de leitura (PAPERCLIP_API_KEY_LEITURA, SUPABASE_SERVICE_ROLE_KEY_WEB) e provedor GitHub no Supabase Auth: ações do fundador, Keychain → GitHub Secrets/painel Supabase, quando estiver no Mac.
  3. Label ratificado e tag decisao-board criados no GitHub e no Paperclip (o coletor não cria; recusa com erro claro se não existirem).

11. Fora de escopo, de propósito

  • Não é tempo real. Retrato a cada 15 min. O padrão que sobrevive nos projetos agentic é estado e exceção, não feed.
  • Não edita nada. Prompt, gate, contrato e código continuam sendo pull request.
  • Não substitui o Paperclip. O quadro segue sendo a fonte da verdade do trabalho; o painel lê dele e escreve nele.
  • Não inventa área. Sustentação, atendimento, vendas e mídias entram quando tiverem gatilho, juiz e sinal.
  • Não define saúde por domínio. O que faz papel de teste em atendimento ou vendas é pergunta aberta; este painel mostra o estado quando existir.
  • Não notifica. Telegram/push continuam sendo os alarmes existentes; a tela é onde se olha, não o que chama.

12. Fatias sugeridas para o plano

Cada fatia entrega algo verificável sozinho e passa pela esteira como PR normal.

  1. db-push real para apps/web (pré-requisito 1).
  2. Contrato + avaliador + validação (config/sinais.yml, scripts/estado/contrato.ts, avaliar-limiar.ts, testes). Sem fonte, sem tabela: só a gramática travada.
  3. Migration das duas tabelas + RLS + pgTAP.
  4. Coletor com fontes github.* e config.* (as que só precisam do GITHUB_TOKEN), workflow, entrada no dead-man. Já produz retratos reais.
  5. Tela níveis 1 e 2 (leitura), redirect de /cockpit.
  6. Fontes paperclip.* e supabase.* (dependem do pré-requisito 2).
  7. Nível 3: trilha, ações Paperclip e tynna_decisions (credencial de servidor), redirect de /m/triage, E2E.
  8. Provedor GitHub no Supabase Auth + ações GitHub com token do usuário (pré-requisito 2).

Só leitura. Toda mudança nestes documentos é pull request.