bitekas/web-hierarchy-server

Exibe toda a hierarquia de delegação de uma sessão de agente como uma página web AO VIVO e auto-atualizável para humanos que não veem o terminal.

O que é web-hierarchy-server?

O `web-hierarchy-server` transforma o rastreamento de trabalho de um agente de IA em uma página web interativa e auto-atualizável, projetada para que um ser humano em outro dispositivo possa monitorar a atividade do agente sem precisar acessar o terminal. A ferramenta lê as transcrições de sessão que o agente já escreveu e constrói um grafo da árvore de spawn, mostrando a sessão raiz e todos os subagentes com seus tipos, descrição de tarefa, modelo, tempos de início/fim e status. A página é servida como um único documento HTML que consulta um endpoint JSON a cada 10 segundos para se atualizar, e possui um filtro que permite visualizar apenas agentes ativos ou todos. O design do servidor separa a entrega da árvore (leve) dos logs (potencialmente pesados), evitando que a página se torne inutilizável com centenas de megabytes de dados. O servidor suporta múltiplas sessões e projetos, é instalável como serviço do sistema e também pode gerar uma versão estática completa para arquivamento.

Funciona com✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/bitekas/agent-fleet/tree/HEAD/.claude/skills/web-hierarchy-server

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Servidor de hierarquia web

O humano para quem um agente trabalha geralmente está em outro dispositivo e não pode ver o terminal dos subagentes. Isso transforma toda a árvore de spawn em uma página viva que eles abrem uma vez e assistem: um grafo clicável no topo, o log completo do agente selecionado abaixo.

O contexto de instalação desta skill - o diretório que fleet onde web-hierarchy-server imprime - contém os dois arquivos desta instalação: CONTEXT.md nomeia o agente, sua porta e sua URL, e config.json contém todos os valores que os scripts leem. config.example.json ao lado deste arquivo documenta cada chave. Os scripts encontram config.json chamando o mesmo comando ou leem o arquivo que WEB_HIERARCHY_CONFIG nomeia quando está definido; um caminho relativo dentro dele é resolvido em relação ao seu próprio diretório.

Execute as verificações e scripts pelo caminho:

"${CLAUDE_SKILL_DIR}/installation/check"     # um servidor está ativo para este diretório de config? Está atualizado?
node "${CLAUDE_SKILL_DIR}/serve.mjs" --help  # a configuração, raiz de transcrição e porta que usaria

A única regra permanente: UM servidor por diretório de config

Um diretório de config (CLAUDE_CONFIG_DIR) é a raiz de transcrição de um agente, e exatamente um servidor o serve. Um segundo servidor na mesma porta é a falha que esta regra existe para prevenir: qualquer processo que ganhe a vinculação serve a página, o outro morre com EADDRINUSE, e do lado do visualizador a página parece bem de qualquer forma. Antes de iniciar um, estabeleça que nenhum servidor já está ativo para aquele diretório de config (installation/check responde isso e nunca inicia nada).

A forma durável é uma unit de serviço (installation/web-hierarchy-server.service.example): inicia com a máquina, Restart=always, ativo desde a inicialização antes de qualquer trabalho ser delegado, e sobrevivendo a reinicializações. Instalar uma requer root, que é uma etapa humana. No macOS é um LaunchAgent no domínio gui do usuário (installation/web-hierarchy-server.plist.example), instalado sem sudo. Onde ninguém o executou, o servidor roda como um processo destacado iniciado uma vez (setsid nohup ... & no Linux, nohup ... & no macOS, que não tem setsid, então sobrevive à sessão que o iniciou) - essa forma funciona e não sobrevive a uma reinicialização, que é exatamente o que installation/check diz em palavras em vez de reportar um verde.

Edições em serve.mjs ou format.mjs são LATENTES por design. O node os carrega uma vez na inicialização do processo, então uma alteração aplicada só alcança uma página em execução no próximo restart. Isso é esperado; não é motivo para reiniciar um servidor que um visualizador está assistindo, e É a razão pela qual um relato "minha correção não funcionou" geralmente é um processo em execução segurando o código antigo.

A árvore é SERVIDA; os logs são BUSCADOS

Esta é a forma de toda a estrutura, e é uma regra de tamanho, não de estilo.

A ÁRVORE de uma sessão custa cerca de um kilobyte por agente, independentemente do que o agente fez. Seus LOGS são ilimitados, e renderizar um arrasta suas imagens junto. Colocar o log de cada agente em cada snapshot torna GET / para uma sessão de 228 agentes 358MB, 110MB deles dados de imagem base64, reenviados a cada 10s de polling. Nada está quebrado, e a página não pode ser aberta.

Portanto, a carga é dividida, e cada rota carrega um tipo de coisa:

RotaCarregaCustos
GET /a árvore inteira, o log da sessão RAIZ e as imagens desse conteúdo inline como data URIsum documento, completo sem um servidor
GET /statea árvore inteira e NENHUM log (?logs=<id>,<id> nomeia exceções); ?view=active restringe o GRÁFICO à visão ativao que o polling de 10s paga, a cada tick, para sempre
GET /agent/<id>exatamente o log renderizado daquele agenteuma busca, quando o visualizador seleciona um bloco
GET /agent/<id>?since=<n>apenas eventos a partir do índice num array vazio para um agente silencioso
GET /img/<hash>uma imagem por hash de conteúdo, immutableuma vez por imagem por visualizador, depois em cache

Duas consequências que vale a pena saber antes de mudar algo aqui:

  • since indexa o próprio count da árvore. Uma linha da árvore carrega a contagem real de eventos do agente mesmo que seu log não tenha sido renderizado, e esse número é o que um cliente devolve como since. Ambos vêm da mesma análise de transcrição, e precisam vir: derive um deles em outro lugar e um cliente silenciosamente pula ou repete eventos, sem nada para ver além de um log sutilmente errado.
  • GET /img/<hash> é endereçado por conteúdo, então uma resposta é armazenada em cache para sempre. É por isso que um hash desconhecido é um 404 e nunca um 200 vazio - um 200 vazio é uma imagem permanentemente quebrada para aquele visualizador, e nenhum recarregamento a conserta.

Nada fica vermelho se essa divisão for perdida. Coloque todos os logs em GET /state, ou imagens inline em cada snapshot, e a árvore está correta, os rótulos estão corretos, a página renderiza, e tem centenas de megabytes. O TAMANHO é a propriedade sob proteção; as pernas de tamanho em serve.test.mjs (6-9) são a única coisa que pode ver uma regressão, e cada uma é um PAR - ausente aqui, presente ali - porque "X está ausente" é verdadeiro por todas as razões erradas.

O que uma página SALVA tem, e o que não tem

GET / é um único documento HTML autônomo: CSS e JS inline, sem ativos externos, e um snapshot embutido que renderiza sem nenhum servidor. O que está embutido é a árvore inteira e o log da sessão raiz, com as imagens desse conteúdo como data URIs.

Não está embutido: o log de todos os outros agentes. Eles não estão no documento; chegam de /agent/<id> na seleção. Portanto, uma página salva em disco, ou cujo servidor morreu, mostra o gráfico completo e o log da sessão raiz, e não mostra nada quando um bloco de subagente é clicado. As imagens dentro desses logs são a mesma história: renderizam de /img/<hash> e não há servidor por trás de um arquivo salvo.

Para um registro congelado de TODO log em um arquivo, use build.mjs - a "bake" estática inclui inline o log de cada agente e cada imagem, porque uma recordação não tem nada para buscar. Esse é o único chamador que pede à snapshot() todos os logs (logsFor: null).

Como ele segue as sessões

Um servidor é de longa duração; uma sessão Claude não é: terminar uma sessão, uma atualização e um reinício de unidade todos cunham um novo id de sessão. Portanto, o servidor re-seleciona a sessão mais recentemente ativa em cada snapshot em vez de resolver uma na inicialização, e serve uma página "aguardando uma sessão..." enquanto nenhuma existe ainda - o estado normal na inicialização. "Mais recentemente ativa" inclui a transcrição RAIZ, não apenas arquivos de subagente: uma sessão recém-iniciada que ainda não delegou é seguida imediatamente e renderizada como uma árvore apenas com a raiz, e a árvore de spawn cresce a partir de sua primeira delegação. Nada precisa ser reiniciado quando uma nova sessão começa; a página detecta a mudança de id e reconstrói seus logs.

--session=<id> fixa uma sessão (uma visão congelada para depuração).

Cada slug de projeto sob o diretório de config é escaneado, não apenas um. A transcrição de uma sessão cai sob o slug de projeto do seu CWD, então uma sessão trabalhando em uma árvore de trabalho git escreve sob o slug da WORKTREE; fixar um slug torna toda sessão da árvore de trabalho invisível enquanto a página ainda renderiza perfeitamente.

Qual agente uma página diz estar mostrando

O bloco raiz é rotulado com o membro da frota ao qual o diretório de config pertence, resolvido através de fleet.by_config_dir no config.json. A chave é derivada, não o nome base bruto, e a derivação é a parte que vale a pena ler:

O fleetKey() do format.mjs pega o nome base do diretório de config, EXCETO quando esse nome base é o literal .claude, onde ele pega o nome base do diretório PAI. Uma frota que dá a cada agente um diretório home coloca todo diretório de config em <home>/<agente>/.claude, então, chaveado pelo nome base, todos colidem em uma entrada e a página de cada agente é rotulada com qualquer agente que essa entrada nomeia.

Essa falha é silenciosa: a página renderiza completamente e diz o nome errado, e um visualizador com duas páginas abertas não consegue distingui-las e não sabe que não consegue. Uma entrada literal .claude é honrada como fallback, então uma instalação de agente único chaveada como .claude mantém seu rótulo; ela não pode distinguir dois agentes e não deve ser usada onde mais de um existe. A mesma chave derivada escolhe a porta padrão dos servers do config.json, pela mesma razão: uma correspondência de nome base daria a cada agente a porta da primeira entrada.

Arquivos

  • serve.mjs - o servidor ao vivo: GET /, GET /state, GET /agent/<id> e GET /img/<hash> (veja a tabela de rotas acima). A ferramenta principal. --port=N, --host=<addr>, --cap=KB, --session=<id>, cada um com padrão do config.json; --help imprime o uso, o caminho de configuração resolvido, a raiz de transcrição e a porta padrão, e não vincula nada. --port=0 pega uma porta efêmera e o banner imprime a porta que realmente obteve.
  • format.mjs - o formatador compartilhado: análise de transcrição, layout da árvore de spawn, HTML por evento, o CSS do tema, e o único lugar onde valores locais são lidos (cfg() sobre o config.json que fleet onde nomeia, ou sobre WEB_HIERARCHY_CONFIG quando está definido).
  • build.mjs - snapshot ESTÁTICO opcional (node build.mjs <out.html>): assa o estado atual em um único arquivo autocontido para um registro congelado e compartilhável. Use para uma recordação, não para a visão ao vivo.
  • identities.json + avatars/ - o registro de nomes de exibição, usado a menos que identities.file nomeie outro.
  • serve.test.mjs + fixtures/ - o teste: inicia um servidor real em uma porta efêmera contra uma árvore de transcrição sintética, afirma a árvore, dirige cada rota (árvore sem logs, log de um agente e de mais ninguém, ?since enviando apenas o que é novo, uma imagem servida hash por byte), e então DELETA a transcrição de um subagente e exige que a árvore E a rota por agente o percam. Seu cabeçalho indica por que cada perna está lá, qual par cada perna de tamanho é metade, e por que o controle negativo é a parte que suporta carga. A perna 11 exige que GET /state?view=active solte o bloco parado que a visão padrão desenha.
  • view.test.mjs - a regra "apenas ativos" sozinha (filterView() sobre uma árvore sintética): blocos vivos, os blocos acima deles e a raiz, com os irmãos e filhos parados ao lado descartados, e "todos" retornando cada linha inalterada.
  • format.test.mjs - como a config é encontrada e lida: através de fleet onde quando WEB_HIERARCHY_CONFIG não está definido (uma frota falsa selecionada por FLEET_BIN), a variável vencendo, uma frota falhando sendo retransmitida em vez de adivinhada, caminhos relativos, o padrão do registro e packages.dir_pattern.
  • installation/ - o template da unit, o procedimento de instalação e check.

O que a página mostra

Ela se parece com o console do Claude Code no tema escuro-para-daltonismo (dark-daltonized) - paleta retirada literalmente do pacote Claude Code (fundo preto, texto monoespaçado branco, bullets de ferramenta em azul de sucesso #3399ff, erros #ff6666, linhas de resultado esmaecidas com expansores "...+N linhas", marcadores de pensamento, mensagens de tarefa e de usuário emolduradas como entrada de console).

  • Grafo da árvore de spawn. A sessão raiz é o nó superior; todo agente sem pai fica pendurado nela. Azul = vivo (com um arco de spinner giratório ao lado do intervalo de tempo; estático sob prefers-reduced-motion), cinza = parado. Clique em um bloco para mostrar o log completo daquele agente abaixo.
  • Um interruptor "todos / apenas ativos" acima do grafo. "todos" é o padrão e desenha todo bloco. "apenas ativos" desenha os blocos azuis, todo bloco acima de um deles, e a raiz: a bandeira de vivo é a que colore um bloco, então o interruptor nunca discorda das cores. Os blocos acima permanecem porque um pai esperando por um filho longo não escreve nada em sua própria transcrição e aparece como parado enquanto o filho roda; sem eles, o filho ficaria pendurado do nada. Com nada vivo, a visão é apenas a raiz. O interruptor restringe o GRÁFICO apenas: o carimbo ainda conta a sessão inteira (mais "N mostrados"), e um link ou #a-<id> para um agente oculto ainda abre seu log. A escolha é lembrada por visualizador no localStorage do navegador; onde o armazenamento está bloqueado, o interruptor ainda funciona para aquela visita. filterView() do format.mjs é a regra; view.test.mjs a fixa.
  • Descrição de tarefa por agente em cada bloco. Fonte da verdade: o description da chamada Agent de spawn, que o harness copia para agent-<id>.meta.json. Fallback quando ausente: a primeira linha do prompt de tarefa do agente. A descrição quebra e o bloco cresce para caber.
  • A última linha de log em cada bloco, como dica de ferramenta ao passar o mouse. Com algumas centenas de agentes, é a única coisa que separa dois blocos com o mesmo tipo e a mesma descrição, e para um agente vivo é o que ele está fazendo agora. Isso viaja na ÁRVORE porque é o único pedaço de conteúdo de log barato o suficiente: texto simples, limitado, nunca HTML - no momento em que renderiza marcação, arrasta imagens e expansores para cada snapshot.
  • Modelo em cada bloco. parseTranscript lê o modelo das viradas do assistente na transcrição (o prefixo claude- removido); quando o agente ainda não respondeu, o alias grosso em seu .meta.json serve como substituto. Um nó sem nenhum dos dois não renderiza linha em vez de um placeholder.
  • Agentes nomeados com avatares. Cada bloco mostra um nome, seu avatar, o agentType, a descrição, o modelo e o intervalo de tempo, então cinco agentes concorrentes de um tipo são distinguíveis de relance.
    • O nome é um rótulo de exibição apenas: atribuído do registro em identities.file como uma função pura de (agentType, ordem de spawn dentro desse tipo), recomputado das transcrições a cada atualização. Nada é armazenado em nenhum lugar, e os agentes não sabem seus próprios nomes - o harness possui as transcrições e este formatador é um observador somente leitura.
    • Ordem de spawn, não um hash do id do agente: com N nomes e N agentes, um hash colide por pigeonhole; ordem de spawn não pode.
    • A ordem de names suporta carga - apenas anexar. Reordenar ou inserir no meio da lista renomeia silenciosamente o grafo de toda sessão passada.
    • avatar = a silhueta do papel; portraits = arte opcional e personalizada para um nome.

Habilidades Relacionadas