Serveur de hiérarchie web
L'humain pour lequel un agent travaille est souvent sur un autre appareil et ne peut pas voir le terminal des sous-agents. Ce skill transforme l'arbre de spawn complet en une page web live qu'il ouvre une fois et regarde : un graphique cliquable en haut, le log complet de l'agent sélectionné en dessous.
Le contexte d'installation de ce skill - le répertoire que fleet where web-hierarchy-server affiche - contient les deux fichiers de cette installation : CONTEXT.md nomme l'agent, son port et son URL, et config.json contient toutes les valeurs que les scripts lisent. config.example.json à côté de ce fichier documente chaque clé. Les scripts trouvent config.json en appelant cette même commande, ou lisent le fichier nommé par WEB_HIERARCHY_CONFIG quand il est défini ; un chemin relatif à l'intérieur est résolu par rapport à son propre répertoire.
Exécutez les vérifications et scripts par chemin :
"${CLAUDE_SKILL_DIR}/installation/check" # un serveur est-il actif pour ce répertoire de configuration, est-il à jour
node "${CLAUDE_SKILL_DIR}/serve.mjs" --help # la configuration, la racine des transcriptions et le port qu'il utiliserait
La règle unique : UN serveur par répertoire de configuration
Un répertoire de configuration (CLAUDE_CONFIG_DIR) est la racine des transcriptions d'un agent, et exactement un serveur le dessert. Un second serveur sur le même port est l'échec que cette règle vise à éviter : quel que soit le processus qui gagne l'attache du port, il sert la page, l'autre meurt avec EADDRINUSE, et du côté du spectateur la page a l'air correcte de toute façon. Avant d'en démarrer un, établissez qu'aucun serveur n'est déjà actif pour ce répertoire de configuration (installation/check répond à cela et ne démarre jamais rien).
La forme durable est une unité de service (installation/web-hierarchy-server.service.example) : démarrage machine, Restart=always, actif depuis le démarrage avant que tout travail ne soit délégué, et survivant aux redémarrages. L'installation nécessite les droits root, ce qui est une étape humaine. Sous macOS, c'est un LaunchAgent dans le domaine utilisateur gui (installation/web-hierarchy-server.plist.example), installé sans sudo. Là où personne ne l'a exécuté, le serveur fonctionne comme un processus détaché démarré une fois (setsid nohup ... & sur Linux, nohup ... & sur macOS, qui n'a pas de setsid, afin qu'il survive à la session qui l'a lancé) - cette forme fonctionne et ne survit pas à un redémarrage, ce que installation/check dit en mots plutôt qu'en rapportant un état vert.
Les modifications de serve.mjs ou format.mjs sont LATENTES par conception. node les charge une fois au démarrage du processus, donc une modification appliquée n'atteint une page en cours d'exécution qu'au prochain redémarrage. C'est attendu ; ce n'est pas une raison pour redémarrer un serveur qu'un spectateur regarde, et c'est BIEN la raison pour laquelle un rapport disant "mon correctif n'a pas pris" est généralement un processus en cours d'exécution qui détient l'ancien code.
L'arbre est SERVI ; les logs sont RÉCUPÉRÉS
C'est la forme de l'ensemble, et c'est une règle de taille, pas de style.
L'ARBRE d'une session coûte environ un kilooctet par agent, quoi que cet agent ait fait. Ses LOGS sont illimités, et leur rendu entraîne leurs images avec eux. Mettre le log de chaque agent dans chaque instantané rend GET / pour une session de 228 agents 358 Mo, dont 110 Mo de données d'images en base64, renvoyées à chaque sondage de 10s. Rien n'est cassé, et la page ne peut pas être ouverte.
La charge utile est donc divisée, et chaque route transporte un type de chose :
| Route | Transporte | Coûts |
|---|---|---|
GET / | l'arbre entier, le log de la session RACINE, et les images de ce contenu sous forme de data URI | un document, complet sans serveur |
GET /state | l'arbre entier et AUCUN log (?logs=<id>,<id> nomme les exceptions) ; ?view=active réduit le GRAPHIQUE à la vue active | ce que paie le sondage de 10s, chaque tic, pour toujours |
GET /agent/<id> | exactement le log rendu de cet agent | une récupération, quand le spectateur sélectionne un bloc |
GET /agent/<id>?since=<n> | seulement les événements à partir de l'index n | un tableau vide pour un agent silencieux |
GET /img/<hash> | une image par son hash de contenu, immutable | une fois par image par spectateur, puis mise en cache |
Deux conséquences à connaître avant de modifier quoi que ce soit ici :
sinceindexe le proprecountde l'arbre. Une ligne d'arbre porte le nombre réel d'événements de l'agent même si son log n'a pas été rendu, et ce nombre est ce que le client renvoie commesince. Les deux proviennent de la même analyse de transcription, et doivent le faire : dériver l'un d'eux ailleurs et un client saute ou répète silencieusement des événements, sans rien voir d'autre qu'un log subtilement erroné.GET /img/<hash>est adressé par contenu, donc une réponse est mise en cache pour toujours. C'est pourquoi un hash inconnu est une 404 et jamais un 200 vide - un 200 vide est une image définitivement cassée pour ce spectateur, et aucun rechargement ne la répare.
Rien ne devient rouge si cette division est perdue. Mettez chaque log dans GET /state, ou inlinez les images dans chaque instantané, et l'arbre est correct, les étiquettes sont correctes, la page s'affiche, et elle fait des centaines de mégaoctets. La TAILLE est la propriété protégée ; les jambes de taille dans serve.test.mjs (6-9) sont la seule chose qui peut voir une régression, et chacune est une PAIRE - absente ici, présente là - car "X est absent" est autrement vrai pour toutes les mauvaises raisons.
Ce qu'une page SAUVEGARDÉE a, et ce qu'elle n'a pas
GET / est un document HTML autonome : CSS et JS inline, aucune ressource externe, et un instantané intégré qui s'affiche sans serveur du tout. Ce qui est intégré est l'arbre entier et le log de la session racine, avec les images de ce contenu sous forme de data URI.
Pas intégré : le log de chaque autre agent. Ceux-ci ne sont pas dans le document ; ils arrivent depuis /agent/<id> lors de la sélection. Donc une page sauvegardée sur le disque, ou dont le serveur est mort, affiche le graphique complet et le log de la session racine, et n'affiche rien quand on clique sur un bloc de sous-agent. Les images à l'intérieur de ces logs sont la même histoire : elles s'affichent depuis /img/<hash> et il n'y a pas de serveur derrière un fichier sauvegardé.
Pour un enregistrement figé de CHAQUE log dans un seul fichier, utilisez build.mjs - la cuisson statique inline le log de chaque agent et chaque image, car un souvenir n'a rien à récupérer. C'est le seul appelant qui demande snapshot() pour tous les logs (logsFor: null).
Comment il suit les sessions
Un serveur est de longue durée ; une session Claude ne l'est pas : la fin d'une session, une mise à jour et un redémarrage d'unité créent tous un nouvel identifiant de session. Donc le serveur re-sélectionne la session la plus récemment active à chaque instantané plutôt que d'en résoudre une au démarrage, et sert une page "en attente d'une session..." tant qu'aucune n'existe encore - l'état normal au démarrage. "La plus récemment active" inclut la transcription RACINE, pas seulement les fichiers de sous-agents : une session fraîchement démarrée qui n'a pas encore délégué est suivie immédiatement et rendue sous forme d'arbre racine uniquement, et l'arbre de spawn grandit à partir de sa première délégation. Rien n'a besoin d'être redémarré quand une nouvelle session commence ; la page détecte le changement d'identifiant et reconstruit ses logs.
--session=<id> épingle une session à la place (une vue figée pour le débogage).
Chaque slug de projet sous le répertoire de configuration est scanné, pas un seul. La transcription d'une session atterrit sous le slug de projet de son CWD, donc une session travaillant dans une arborescence de travail git écrit sous le slug de l'ARBO ; épingler un slug rend chaque session d'arborescence invisible alors que la page s'affiche encore parfaitement.
Quel agent une page dit afficher
Le bloc racine est étiqueté avec le membre de la flotte auquel appartient le répertoire de configuration, résolu via config.json's fleet.by_config_dir. La clé est dérivée, pas le nom de base brut, et la dérivation est la partie qui vaut la peine d'être lue :
fleetKey()deformat.mjsprend le nom de base du répertoire de configuration, SAUF quand ce nom de base est le littéral.claude, où il prend le nom de base du répertoire PARENT à la place. Une flotte qui donne à chaque agent un répertoire personnel met chaque répertoire de configuration à<home>/<agent>/.claude, donc clé par nom de base, tous entrent en collision sur une seule entrée et la page de chaque agent est étiquetée avec l'agent que cette entrée nomme.
Cet échec est silencieux : la page s'affiche complètement et dit le mauvais nom, et un spectateur avec deux pages ouvertes ne peut pas les distinguer et ne sait pas qu'il ne le peut pas. Une entrée littérale .claude est honorée comme solution de repli, donc une installation mono-agent clé .claude conserve son étiquette ; elle ne peut pas distinguer deux agents et ne devrait pas être utilisée là où il en existe plus d'un. La même clé dérivée choisit le port par défaut dans config.json's servers, pour la même raison : une correspondance de nom de base donnerait à chaque agent le port de la première entrée.
Fichiers
serve.mjs- le serveur live :GET /,GET /state,GET /agent/<id>etGET /img/<hash>(voir le tableau des routes ci-dessus). L'outil principal.--port=N,--host=<addr>,--cap=KB,--session=<id>, chacun par défaut depuisconfig.json;--helpaffiche l'utilisation, le chemin de configuration résolu, la racine des transcriptions et le port par défaut, et n'attache rien.--port=0prend un port éphémère et la bannière affiche celui effectivement obtenu.format.mjs- le formateur partagé : analyse des transcriptions, disposition de l'arbre de spawn, HTML par événement, le thème CSS, et l'endroit unique où les valeurs locales sont lues (cfg()sur leconfig.jsonquefleet wherenomme, ou surWEB_HIERARCHY_CONFIGquand celui-ci est défini).build.mjs- instantané STATIQUE optionnel (node build.mjs <out.html>) : cuit l'état actuel dans un fichier autonome pour un enregistrement figé et partageable. Utilisez pour un souvenir, pas pour la vue live.identities.json+avatars/- le registre des noms d'affichage, utilisé sauf siidentities.fileen nomme un autre.serve.test.mjs+fixtures/- le test : il démarre un vrai serveur sur un port éphémère contre un arbre de transcription synthétique, vérifie l'arbre, pilote chaque route (arbre sans logs, le log d'un agent et personne d'autre,?sincelivrant seulement ce qui est nouveau, une image servie octet par octet par hash), puis SUPPRIME la transcription d'un sous-agent et exige que l'arbre ET la route par agent la perdent. Son en-tête indique pourquoi chaque jambe est là, quelle paire chaque jambe de taille est la moitié, et pourquoi le contrôle négatif est la partie porteuse de charge. La jambe 11 exige queGET /state?view=activesupprime le bloc arrêté que la vue par défaut dessine.view.test.mjs- la règle "actifs uniquement" en elle-même (filterView()sur un arbre synthétique) : les blocs vivants, les blocs au-dessus d'eux et la racine, avec les frères et enfants arrêtés à côté d'eux supprimés, et "tous" renvoyant chaque ligne inchangée.format.test.mjs- comment la configuration est trouvée et lue : viafleet wherequandWEB_HIERARCHY_CONFIGn'est pas défini (une fausse flotte sélectionnée parFLEET_BIN), la variable gagnante, une flotte en échec relayée plutôt que devinée, les chemins relatifs, le registre par défaut, etpackages.dir_pattern.installation/- le modèle d'unité, la procédure d'installation, etcheck.
Ce que la page montre
Elle ressemble à la console Claude Code dans le thème sombre pour daltoniens (dark-daltonized) - palette reprise textuellement du bundle Claude Code (fond noir, texte monospace blanc, puces d'outils en bleu succès #3399ff, erreurs #ff6666, lignes de résultats atténuées avec des développeurs "...+N lignes", marqueurs de réflexion, messages de tâche et d'utilisateur encadrés comme une entrée console).
- Graphique de l'arbre de spawn. La session racine est le nœud supérieur ; chaque agent sans parent est suspendu à lui. Bleu = vivant (avec un arc de spinner rotatif à côté de la plage horaire ; statique sous
prefers-reduced-motion), gris = arrêté. Cliquez sur un bloc pour afficher le log complet de cet agent ci-dessous. - Un interrupteur "tous / actifs uniquement" au-dessus du graphique. "tous" est le défaut et dessine chaque bloc. "actifs uniquement" dessine les blocs bleus, chaque bloc au-dessus de l'un d'eux, et la racine : le drapeau vivant est celui qui colore un bloc, donc l'interrupteur n'est jamais en désaccord avec les couleurs. Les blocs au-dessus restent car un parent attendant un enfant long n'écrit rien dans sa propre transcription et est considéré comme arrêté pendant que l'enfant s'exécute ; sans eux, l'enfant pendrait de rien. Avec rien de vivant, la vue est la racine seule. L'interrupteur réduit le GRAPHIQUE seulement : le timbre compte toujours la session entière (plus "N affichés"), et un lien depuis ou
#a-<id>vers un agent caché ouvre toujours son log. Le choix est mémorisé par spectateur dans lelocalStoragedu navigateur ; là où le stockage est bloqué, l'interrupteur fonctionne encore pour cette visite.filterView()deformat.mjsest la règle ;view.test.mjsla fige. - Description de tâche par agent sur chaque bloc. Source de vérité : la
descriptionde l'appelAgentqui l'a créé, que le harnais copie dansagent-<id>.meta.json. Solution de repli en cas d'absence : la première ligne de l'invite de tâche de l'agent. La description se plie et le bloc grandit pour l'adapter. - La dernière ligne de log sur chaque bloc, comme infobulle au survol. À quelques centaines d'agents, c'est la seule chose qui sépare deux blocs avec le même type et la même description, et pour un agent vivant, c'est ce qu'il est en train de faire maintenant. Elle est transportée dans l'ARBRE parce que c'est le seul contenu de log assez bon marché pour cela : texte brut, limité, jamais HTML - dès qu'elle rend du balisage, elle entraîne des images et des développeurs dans chaque instantané.
- Modèle sur chaque bloc.
parseTranscriptlit le modèle sur les tours d'assistant de la transcription (le préfixeclaude-supprimé) ; quand l'agent n'a pas encore répondu, l'alias grossier dans son.meta.jsonsert de remplacement. Un nœud sans ni l'un ni l'autre n'affiche aucune ligne plutôt qu'un espace réservé. - Agents nommés avec avatars. Chaque bloc montre un nom, son avatar, l'agentType, la description, le modèle et la plage horaire, donc cinq agents simultanés d'un type sont distinguables en un coup d'œil.
- Le nom est une étiquette d'affichage uniquement : attribué depuis le registre à
identities.filecomme une fonction pure de (agentType, ordre de spawn dans ce type), recalculé à partir des transcriptions à chaque rafraîchissement. Rien n'est stocké nulle part, et les agents ne connaissent pas leurs propres noms - le harnais possède les transcriptions et ce formateur est un observateur en lecture seule. - Ordre de spawn, pas un hash de l'identifiant de l'agent : avec N noms et N agents, un hash entre en collision par principe du tiroir ; l'ordre de spawn ne le peut pas.
- L'ordre de
namesest porteur de charge - ajout seulement. Réorganiser ou insérer au milieu du liste renomme silencieusement le graphique de chaque session passée. avatar= la silhouette du rôle ;portraits= art optionnel sur mesure pour un nom. L'avatar identifie…
- Le nom est une étiquette d'affichage uniquement : attribué depuis le registre à