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:
| Rota | Carrega | Custos |
|---|---|---|
GET / | a árvore inteira, o log da sessão RAIZ e as imagens desse conteúdo inline como data URIs | um documento, completo sem um servidor |
GET /state | a árvore inteira e NENHUM log (?logs=<id>,<id> nomeia exceções); ?view=active restringe o GRÁFICO à visão ativa | o que o polling de 10s paga, a cada tick, para sempre |
GET /agent/<id> | exatamente o log renderizado daquele agente | uma busca, quando o visualizador seleciona um bloco |
GET /agent/<id>?since=<n> | apenas eventos a partir do índice n | um array vazio para um agente silencioso |
GET /img/<hash> | uma imagem por hash de conteúdo, immutable | uma vez por imagem por visualizador, depois em cache |
Duas consequências que vale a pena saber antes de mudar algo aqui:
sinceindexa o própriocountda á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 comosince. 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()doformat.mjspega 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>eGET /img/<hash>(veja a tabela de rotas acima). A ferramenta principal.--port=N,--host=<addr>,--cap=KB,--session=<id>, cada um com padrão doconfig.json;--helpimprime o uso, o caminho de configuração resolvido, a raiz de transcrição e a porta padrão, e não vincula nada.--port=0pega 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 oconfig.jsonquefleet ondenomeia, ou sobreWEB_HIERARCHY_CONFIGquando 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 queidentities.filenomeie 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,?sinceenviando 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 queGET /state?view=activesolte 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 defleet ondequandoWEB_HIERARCHY_CONFIGnão está definido (uma frota falsa selecionada porFLEET_BIN), a variável vencendo, uma frota falhando sendo retransmitida em vez de adivinhada, caminhos relativos, o padrão do registro epackages.dir_pattern.installation/- o template da unit, o procedimento de instalação echeck.
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 nolocalStoragedo navegador; onde o armazenamento está bloqueado, o interruptor ainda funciona para aquela visita.filterView()doformat.mjsé a regra;view.test.mjsa fixa. - Descrição de tarefa por agente em cada bloco. Fonte da verdade: o
descriptionda chamadaAgentde spawn, que o harness copia paraagent-<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.
parseTranscriptlê o modelo das viradas do assistente na transcrição (o prefixoclaude-removido); quando o agente ainda não respondeu, o alias grosso em seu.meta.jsonserve 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.filecomo 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
namessuporta 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.
- O nome é um rótulo de exibição apenas: atribuído do registro em