bitekas/web-hierarchy-server

Affiche toute la hiérarchie de délégation d'une session d'agent sous forme de page web LIVE et auto-mise à jour pour un humain qui ne peut pas voir le terminal.

Qu'est-ce que web-hierarchy-server ?

Ce skill transforme la session d'un agent IA (comme Claude Code) en une page web interactive, auto-mise à jour toutes les 10 secondes, visible sur n'importe quel appareil. Il lit les transcriptions générées par l'agent, construit un arbre de spawn (ou arbre de délégation) complet incluant la session racine et tous les sous-agents, et sert une page HTML unique avec graphique cliquable et logs. L'architecture sépare les données pour éviter des charges de plusieurs centaines de mégaoctets : l'arbre et les logs de la session racine sont servis directement dans la page principale (`GET /`), tandis que les logs des sous-agents sont chargés à la demande (`GET /agent/<id>`). Il supporte plusieurs mises en service (service systemd, LaunchAgent macOS, ou processus détaché) et offre des modes d'affichage (tous les agents ou uniquement les actifs). C'est un outil essentiel pour les développeurs qui délèguent des tâches à des agents et veulent surveiller leur progression sans accès au terminal.

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

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

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 :

RouteTransporteCoûts
GET /l'arbre entier, le log de la session RACINE, et les images de ce contenu sous forme de data URIun document, complet sans serveur
GET /statel'arbre entier et AUCUN log (?logs=<id>,<id> nomme les exceptions) ; ?view=active réduit le GRAPHIQUE à la vue activece que paie le sondage de 10s, chaque tic, pour toujours
GET /agent/<id>exactement le log rendu de cet agentune récupération, quand le spectateur sélectionne un bloc
GET /agent/<id>?since=<n>seulement les événements à partir de l'index nun tableau vide pour un agent silencieux
GET /img/<hash>une image par son hash de contenu, immutableune fois par image par spectateur, puis mise en cache

Deux conséquences à connaître avant de modifier quoi que ce soit ici :

  • since indexe le propre count de 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 comme since. 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() de format.mjs prend 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> et GET /img/<hash> (voir le tableau des routes ci-dessus). L'outil principal. --port=N, --host=<addr>, --cap=KB, --session=<id>, chacun par défaut depuis config.json ; --help affiche l'utilisation, le chemin de configuration résolu, la racine des transcriptions et le port par défaut, et n'attache rien. --port=0 prend 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 le config.json que fleet where nomme, ou sur WEB_HIERARCHY_CONFIG quand 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 si identities.file en 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, ?since livrant 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 que GET /state?view=active supprime 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 : via fleet where quand WEB_HIERARCHY_CONFIG n'est pas défini (une fausse flotte sélectionnée par FLEET_BIN), la variable gagnante, une flotte en échec relayée plutôt que devinée, les chemins relatifs, le registre par défaut, et packages.dir_pattern.
  • installation/ - le modèle d'unité, la procédure d'installation, et check.

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 le localStorage du navigateur ; là où le stockage est bloqué, l'interrupteur fonctionne encore pour cette visite. filterView() de format.mjs est la règle ; view.test.mjs la fige.
  • Description de tâche par agent sur chaque bloc. Source de vérité : la description de l'appel Agent qui l'a créé, que le harnais copie dans agent-<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. parseTranscript lit le modèle sur les tours d'assistant de la transcription (le préfixe claude- supprimé) ; quand l'agent n'a pas encore répondu, l'alias grossier dans son .meta.json sert 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.file comme 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 names est 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…

Skills associés