Communityコーディング&開発github.com

bitekas/web-hierarchy-server

エージェントセッションの委譲階層全体をライブで自己更新するWebページとして表示し、ターミナルを見られない人間がブラウザから全体を監視できるようにします。

web-hierarchy-server とは?

このスキルは、AIエージェント(Claude Codeなど)の実行中に生成されるスーパーバイザー・サブエージェントの階層構造(スポーンツリー)を、リアルタイムで更新されるWebページとして提供します。ターミナルを直接見られないユーザーが別のデバイスからでも、エージェントの状態(生存・停止)、タスク内容、モデル名、開始・終了時刻、最新のログ行などを一覧できるグラフと、各エージェントの詳細ログを参照できます。Node.jsサーバー(serve.mjs)がエージェントのトランスクリプトを読み取り、ツリー全体をJSONスナップショットとして提供し、フロントエンドが10秒ごとにポーリングしてページを自己更新します。デフォルトでは全エージェントを表示し、アクティブなものだけに絞る切り替えも可能です。このアーキテクチャは、ログサイズが無制限に増加する問題を回避するために、ツリーとログを別々のルートで提供する設計を採用しており、大規模セッションでもページが開けなくなるのを防ぎます。

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

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Web階層サーバー

エージェントが働く人間は、しばしば別のデバイスを使用しており、サブエージェントのターミナルを見ることができません。このスキルは、スポーンツリー全体を、一度開けば見続けられる1つのライブページに変換します。上部にクリック可能なグラフ、下部に選択したエージェントの完全なログが表示されます。

このスキルのインストールコンテキスト(fleet where web-hierarchy-serverが出力するディレクトリ)には、このインストールの2つのファイルが格納されています。CONTEXT.mdはエージェント名、そのポート、URLを定義し、config.jsonはスクリプトが読み取るすべての値を保持します。このファイルの隣にあるconfig.example.jsonは各キーを文書化しています。スクリプトは同じコマンドに問い合わせることでconfig.jsonを見つけるか、WEB_HIERARCHY_CONFIGが設定されている場合はそのファイルを読み取ります。内部の相対パスは、そのファイル自身のディレクトリに対して解決されます。

チェックとスクリプトはパスを指定して実行します:

"${CLAUDE_SKILL_DIR}/installation/check"     # この設定ディレクトリに対してサーバーが起動しているか、最新か
node "${CLAUDE_SKILL_DIR}/serve.mjs" --help  # 使用する設定、トランスクリプトルート、ポート

唯一のルール:設定ディレクトリにつき1つのサーバー

設定ディレクトリ(CLAUDE_CONFIG_DIR)は1つのエージェントのトランスクリプトルートであり、それを提供するサーバーは正確に1つだけです。同じポート上の2番目のサーバーは、このルールが防ごうとする障害です。どちらのプロセスがバインドに成功してもページは提供され、もう一方はEADDRINUSEで終了します。ビューア側からはどちらでもページは問題なく見えます。起動する前に、その設定ディレクトリに対してサーバーが既に起動していないことを確認してください(installation/checkが回答し、何も起動しません)。

永続的な形式はサービスユニットです(installation/web-hierarchy-server.service.example):マシン起動時、Restart=always、ブートからすべての作業が委任される前に起動し、再起動後も維持されます。インストールにはroot権限が必要であり、これは人間の作業です。macOSでは、ユーザーのguiドメイン内のLaunchAgent(installation/web-hierarchy-server.plist.example)として、sudoなしでインストールされます。誰も実行していない場合、サーバーはデタッチされたプロセスとして1回起動されます(Linuxではsetsid nohup ... &、macOSではnohup ... & - setsidがないため、起動したセッションよりも長く存続します)。この形式は機能し、再起動後は持続しません。これはinstallation/checkが「緑」ではなく言葉で報告する内容そのものです。

serve.mjsまたはformat.mjsへの編集は、設計上LATENT(潜在)です。 nodeはプロセス起動時に一度だけそれらをロードするため、適用された変更は次の再起動まで実行中のページに反映されません。これは想定内であり、ビューアが見ているサーバーを再起動する理由にはなりません。また、「修正が適用されなかった」という報告が、通常は古いコードを保持した実行中プロセスによるものである理由でもあります。

ツリーはSERVED(提供)され、ログはFETCHED(取得)される

これが全体の形状であり、スタイルではなくサイズのルールです。

セッションのツリーは、エージェントが何をしたかにかかわらず、エージェントあたり約1キロバイトです。ログは無制限であり、レンダリングすると画像も一緒に取り込まれます。すべてのエージェントのログをすべてのスナップショットに含めると、228エージェントのセッションではGET /が358MBになり、そのうち110MBはbase64画像データで、10秒ごとのポーリングで再送信されます。何も壊れてはいませんが、ページを開くことができません。

そのため、ペイロードは分割され、各ルートは1種類のものを運びます:

ルート運ぶものコスト
GET /ツリー全体、ルートセッションのログ、およびそのコンテンツの画像をインラインのデータURIとしてサーバーなしで完結する1つのドキュメント
GET /stateツリー全体とログなし(?logs=<id>,<id>で例外を指定); ?view=activeはグラフをアクティブビューに限定10秒ポーリングが毎ティック、永遠に支払うコスト
GET /agent/<id>そのエージェントのレンダリングされたログのみビューアがブロックを選択したときの1回のフェッチ
GET /agent/<id>?since=<n>インデックスnからのイベントのみ静かなエージェントには空の配列
GET /img/<hash>コンテンツハッシュによる1つの画像、immutableビューアあたり画像ごとに1回、その後キャッシュ

ここで何かを変更する前に知っておくべき2つの結果:

  • sinceはツリー自身のcountをインデックスします。 ツリー行は、ログがレンダリングされていなくてもエージェントの実際のイベント数を保持し、その数値がクライアントがsinceとして返すものです。両方とも同じトランスクリプトパースから得られ、そうでなければなりません。どこか別の場所で一方を派生させると、クライアントは静かにイベントをスキップまたは反復し、微妙に間違ったログ以外は何も見えません。
  • GET /img/<hash>はコンテンツアドレス指定されているため、応答は永遠にキャッシュされます。 そのため、未知のハッシュは404であり、空の200ではありません。空の200はそのビューアに対して永久的に壊れた画像となり、リロードでも修正できません。

この分割が失われても、何も赤くなりません。 すべてのログをGET /stateに入れたり、画像をすべてのスナップショットにインライン化したりしても、ツリーは正しく、ラベルは正しく、ページはレンダリングされますが、数百メガバイトになります。サイズが保護対象のプロパティです。serve.test.mjs(6-9行)のサイズチェックだけがその退行を検出でき、それぞれはペアです。ここにない、あそこにある、なぜなら「Xがない」はそれ以外のすべての間違った理由でも真だからです。

保存されたページが持つものと持たないもの

GET /は1つのスタンドアロンHTMLドキュメントです。インラインCSSとJS、外部アセットなし、サーバーなしでレンダリングされる焼き込みスナップショットを含みます。焼き込まれるのはツリー全体とルートセッションのログで、そのコンテンツの画像はデータURIとして含まれます。

焼き込まれないもの:他のすべてのエージェントのログ。 これらはドキュメント内になく、選択時に/agent/<id>から取得されます。そのため、ディスクに保存されたページやサーバーが停止したページは、完全なグラフとルートセッションのログを表示しますが、サブエージェントブロックをクリックしても何も表示しません。これらのログ内の画像も同様です。/img/<hash>からレンダリングされますが、保存されたファイルの背後にはサーバーがありません。

すべてのログを1つのファイルに凍結した記録として、build.mjsを使用してください。静的ベイクはすべてのエージェントのログとすべての画像をインライン化します。記念品は何もフェッチする必要がないからです。これが、snapshot()にすべてのログ(logsFor: null)を要求する唯一の呼び出し元です。

セッションの追跡方法

サーバーは長命です。Claudeセッションはそうではありません。セッションの終了、更新、ユニットの再起動はすべて新しいセッションIDを作成します。そのため、サーバーは起動時に1つを解決するのではなく、すべてのスナップショットで最新アクティブなセッションを再選択し、まだ存在しない場合は「セッションを待機中...」というページを提供します(起動時の通常状態)。「最新アクティブ」にはルートトランスクリプトも含まれます。まだ委任していない新しく開始されたセッションは即座に追跡され、ルートのみのツリーとしてレンダリングされ、最初の委任からスポーンツリーが成長します。新しいセッションが開始されても何も再起動する必要はありません。ページはID変更を検出し、ログを再構築します。

--session=<id>は代わりに1つのセッションを固定します(デバッグ用の固定ビュー)。

設定ディレクトリ下のすべてのプロジェクトスラッグがスキャンされます。1つだけではありません。セッションのトランスクリプトは、そのCWDのプロジェクトスラッグの下に配置されるため、gitワークツリーで作業しているセッションはワークツリーのスラッグの下に書き込まれます。1つのスラッグを固定すると、すべてのワークツリーセッションが見えなくなりますが、ページは完全にレンダリングされ続けます。

ページが表示しているエージェント

ルートブロックには、設定ディレクトリが属するfleetメンバーのラベルが付けられ、config.jsonのfleet.by_config_dirを通じて解決されます。キーは派生され、生のベース名ではありません。その派生が読む価値のある部分です:

format.mjsのfleetKey()は、設定ディレクトリのベース名を取得します。ただし、そのベース名がリテラルの.claudeである場合を除き、その場合は代わりに親ディレクトリのベース名を取得します。各エージェントにホームディレクトリを与えるfleetは、すべての設定ディレクトリを<home>/<agent>/.claudeに配置するため、ベース名でキー付けすると、すべてが1つのエントリに衝突し、すべてのエージェントのページがそのエントリが名前を付けるエージェントでラベル付けされます。

その失敗は沈黙しています。ページは完全にレンダリングされますが、間違った名前を表示し、2つのページを開いたビューアはそれらを区別できず、区別できないことも知りません。リテラルの.claudeエントリはフォールバックとして尊重されるため、.claudeでキー付けされたシングルエージェントインストールはラベルを保持します。2つのエージェントを区別できず、複数存在する場合は使用すべきではありません。同じ派生キーがconfig.jsonのserversからデフォルトポートを選択します。同じ理由で、ベース名の一致はすべてのエージェントに最初のエントリのポートを渡すからです。

ファイル

  • serve.mjs - ライブサーバー:GET /、GET /state、GET /agent/<id>、GET /img/<hash>(上記のルートテーブルを参照)。主要ツール。 --port=N、--host=<addr>、--cap=KB、--session=<id>。各々はconfig.jsonからデフォルト設定されます。--helpは使用法、解決された設定パス、トランスクリプトルート、デフォルトポートを表示し、何もバインドしません。--port=0は一時ポートを取得し、バナーは実際に取得したポートを表示します。
  • format.mjs - 共有フォーマッタ:トランスクリプト解析、スポーンツリーレイアウト、イベントごとのHTML、テーマCSS、およびローカル値が読み取られる1つの場所(fleet whereが指定するconfig.json、またはWEB_HIERARCHY_CONFIGが設定されている場合のそのファイルからのcfg())。
  • build.mjs - オプションの静的スナップショット(node build.mjs <out.html>):現在の状態を1つの自己完結型ファイルにベイクし、凍結された共有可能なレコードを作成します。ライブビューではなく記念品に使用します。
  • identities.json + avatars/ - 表示名リスト。identities.fileが別のファイルを指定しない限り使用されます。
  • serve.test.mjs + fixtures/ - テスト:実際のサーバーを一時ポートで起動し、合成トランスクリプトツリーに対して、ツリーをアサートし、すべてのルートを駆動します(ログなしのツリー、1つのエージェントのログのみ、?sinceで新しいもののみを配信、ハッシュでバイト単位で提供される画像)。その後、1つのサブエージェントのトランスクリプトを削除し、ツリーとエージェントごとのルートの両方からそれが消えることを要求します。そのヘッダーは、各レッグがなぜ存在するか、各サイズレッグがどのペアの半分か、そしてなぜネガティブコントロールが耐荷重部分であるかを述べています。レッグ11は、GET /state?view=activeがデフォルトビューが描画する停止ブロックをドロップすることを要求します。
  • view.test.mjs - 単独の「アクティブのみ」ルール(合成ツリーに対するfilterView()):生存ブロック、その上のブロック、およびルート。隣の停止した兄弟と子はドロップされ、「all」はすべての行を変更せずに返します。
  • format.test.mjs - 設定が見つかり読み取られる方法:WEB_HIERARCHY_CONFIGが設定されていない場合のfleet where経由(FLEET_BINで選択された偽のfleet)、変数の優先、失敗したfleetの推測ではなく中継、相対パス、ロスターのデフォルト、およびpackages.dir_pattern。
  • installation/ - ユニットテンプレート、インストール手順、およびcheck。

ページが表示するもの

Claude Codeコンソールの暗色-色覚異常対応(dark-daltonized)テーマと同様に読み取れます。パレットはClaude Codeバンドルからそのまま取得されています(黒背景、白の等幅フォント、ツール箇条書きは成功青#3399ff、エラーは#ff6666、結果行は「... +N行」展開子で薄暗く表示、思考マーカー、タスクおよびユーザーメッセージはコンソール入力のようにフレーム化)。

  • スポーンツリーグラフ。 ルートセッションはトップノードです。親のないエージェントはすべてそこからぶら下がります。青 = 生存(時間範囲の横に回転するスピナーアークあり。prefers-reduced-motionの下では静的)、灰色 = 停止。ブロックをクリックすると、そのエージェントの完全なログが下に表示されます。
  • グラフの上の「すべて / アクティブのみ」切り替え。 「すべて」がデフォルトで、すべてのブロックを描画します。「アクティブのみ」は青いブロック、それらの上のすべてのブロック、およびルートを描画します。生存フラグがブロックを色付けするため、切り替えが色と矛盾することはありません。上のブロックは、長い子を待っている親は自身のトランスクリプトに何も書き込まず、子が実行中は停止として読み取られるため、残されます。それらがなければ、子は何もないところからぶら下がることになります。何も生存していない場合、ビューはルートのみです。切り替えはグラフのみを絞り込みます。スタンプは依然としてセッション全体をカウントし(「N表示」も)、隠れたエージェントへのリンクまたは#a-<id>は依然としてそのログを開きます。選択はブラウザのlocalStorageにビューアごとに記憶されます。ストレージがブロックされている場合でも、切り替えはその訪問に対して機能します。format.mjsのfilterView()がルールです。view.test.mjsがそれを固定します。
  • すべてのブロック上のエージェントごとのタスク説明。 信頼できる情報源:スポーンするAgentコールのdescription。ハーネスがこれをagent-<id>.meta.jsonにコピーします。存在しない場合のフォールバック:エージェントのタスクプロンプトの最初の行。説明は折り返され、ブロックはそれに合わせて拡大します。
  • すべてのブロック上の最後のログ行(ホバーツールチップとして)。 数百のエージェントでは、同じタイプと説明を持つ2つのブロックを区別する唯一のものです。生存エージェントの場合、それが現在行っていることです。これはツリーに含まれます。なぜなら、それが十分に安価なログコンテンツの1つだからです。プレーンテキスト、切り詰められ、決してHTMLではありません。マークアップをレンダリングすると、画像と展開子がすべてのスナップショットに引き込まれます。
  • すべてのブロック上のモデル。 parseTranscriptはトランスクリプトのアシスタントターンからモデルを読み取ります(claude-プレフィックスは除去されます)。エージェントがまだ応答していない場合、その.meta.jsonの粗いエイリアスが代わりに使用されます。どちらもないノードは、プレースホルダーではなく行をレンダリングしません。
  • アバター付きの名前付きエージェント。 各ブロックは名前、アバター、agentType、説明、モデル、時間範囲を表示するため、1つのタイプの5つの同時実行エージェントを一目で区別できます。
    • 名前は表示ラベルのみです。identities.fileのロスターから(agentType、そのタイプ内のスポーン順序)の純粋関数として割り当てられ、リフレッシュのたびにトランスクリプトから再計算されます。どこにも保存されず、エージェントは自分の名前を知りません。ハーネスがトランスクリプトを所有し、このフォーマッタは読み取り専用のオブザーバーです。
    • スポーン順序。エージェントIDのハッシュではありません。N個の名前とN個のエージェントで、ハッシュはピジョンホール原理で衝突します。スポーン順序は衝突しません。
    • namesの順序は耐荷重です - 追加専用。 リストの途中の順序変更または挿入は、過去のすべてのセッションのグラフを静かに名前変更します。
    • avatar = 役割のシルエット。portraits = 名前に対するオプションの特注アート。 アバターはエージェントタイプを識別します。ポートレートはそのタイプ内の個々のエージェントを識別します。両方ともidentities.jsonから提供されます。

[以下、元のマークダウンは省略されていますが、翻訳は元のコンテンツに従います]

関連スキル