웹 계층 서버
에이전트를 위해 작업하는 인간 사용자는 종종 다른 기기를 사용하며 하위 에이전트의 터미널을 볼 수 없습니다. 이 스킬은 전체 스폰 트리를 하나의 라이브 페이지로 전환하여 사용자가 한 번 열어서 지켜볼 수 있게 합니다: 상단의 클릭 가능한 그래프와 선택된 에이전트의 전체 로그가 아래에 표시됩니다.
이 스킬의 설치 컨텍스트 - fleet where web-hierarchy-server 명령어가 출력하는 디렉토리 - 는 이 설치의 두 파일을 보관합니다:
CONTEXT.md는 에이전트 이름, 포트 및 URL을 명시하고, config.json은 스크립트가 읽는 모든 값을 보관합니다.
이 파일 옆에 있는 config.example.json은 각 키를 문서화합니다. 스크립트는 동일한 명령어를 통해 config.json을 찾거나, WEB_HIERARCHY_CONFIG 환경 변수가 설정된 경우 해당 파일을 읽습니다;
내부의 상대 경로는 해당 디렉토리를 기준으로 해석됩니다.
경로를 사용하여 검사와 스크립트를 실행하세요:
"${CLAUDE_SKILL_DIR}/installation/check" # 이 config 디렉토리에 대해 서버가 실행 중인지, 최신 상태인지 확인
node "${CLAUDE_SKILL_DIR}/serve.mjs" --help # 사용할 config, 트랜스크립트 루트 및 포트를 표시
유일한 규칙: config 디렉토리당 하나의 서버
config 디렉토리(CLAUDE_CONFIG_DIR)는 하나의 에이전트 트랜스크립트 루트이며, 정확히 하나의 서버가 이를 제공합니다.
동일한 포트에서 두 번째 서버가 실행되는 것은 이 규칙이 방지하고자 하는 실패입니다:
어느 프로세스가 바인딩에 성공하든 페이지는 제공되며, 다른 프로세스는 EADDRINUSE 오류로 종료됩니다.
뷰어 입장에서는 페이지가 어느 쪽이든 정상으로 보입니다. 서버를 시작하기 전에,
해당 config 디렉토리에 이미 서버가 실행 중인지 확인하세요 (installation/check가 답을 제공하며, 아무것도 시작하지 않습니다).
지속 가능한 형태는 서비스 유닛입니다 (installation/web-hierarchy-server.service.example):
시스템 시작 시 실행, Restart=always, 작업이 위임되기 전에 부팅 시부터 실행되며,
재부팅 후에도 유지됩니다. 설치하려면 루트 권한이 필요하며, 이는 인간의 단계입니다. macOS에서는
사용자의 gui 도메인에서 LaunchAgent (installation/web-hierarchy-server.plist.example)로 설치되며, sudo 없이 설치됩니다.
어디에서도 실행되지 않은 경우, 서버는 분리된 프로세스로 한 번 시작됩니다
(Linux에서는 setsid nohup ... &, macOS에서는 nohup ... & - setsid가 없으므로 실행한 세션이 종료되어도 유지됩니다) -
이 형태는 작동하며 재부팅 후에는 유지되지 않습니다. 이는 installation/check가 정확히
녹색 보고 대신 말로 설명하는 내용입니다.
serve.mjs 또는 format.mjs에 대한 편집은 의도적으로 지연됩니다. node는 프로세스 시작 시 한 번만 로드하므로,
변경 사항은 다음 재시작 시에만 실행 중인 페이지에 도달합니다. 이는 예상된 동작이며,
뷰어가 보고 있는 서버를 재시작할 이유가 되지 않습니다. 또한 "내 수정 사항이 적용되지 않았다"는 보고가
일반적으로 이전 코드를 보유한 실행 중인 프로세스 때문인 이유이기도 합니다.
트리는 제공되고, 로그는 가져옵니다
이것은 전체 구조의 형태이며, 스타일이 아닌 크기 규칙입니다.
세션의 트리는 에이전트가 수행한 작업에 관계없이 에이전트당 약 1KB의 비용이 듭니다.
로그는 무한하며, 렌더링 시 해당 에이전트의 이미지를 함께 가져옵니다. 모든 에이전트의 로그를 모든 스냅샷에 포함시키면
228개 에이전트 세션의 GET /는 358MB가 되며, 이 중 110MB는 base64 이미지 데이터로,
10초마다 재전송됩니다. 아무것도 고장나지 않았지만, 페이지를 열 수 없습니다.
따라서 페이로드는 분할되며, 각 경로는 한 가지 유형의 데이터를 전달합니다:
| 경로 | 전달 | 비용 |
|---|---|---|
GET / | 전체 트리, ROOT 세션의 로그, 해당 콘텐츠의 이미지(data URI로 인라인) | 서버 없이 완전한 하나의 문서 |
GET /state | 전체 트리와 로그 없음 (?logs=<id>,<id>는 예외 지정); ?view=active는 그래프를 활성 보기로 제한 | 10초 폴링이 영원히 매 틱마다 지불하는 비용 |
GET /agent/<id> | 정확히 해당 에이전트의 렌더링된 로그 | 뷰어가 블록을 선택할 때 한 번 가져옴 |
GET /agent/<id>?since=<n> | 인덱스 n부터의 이벤트만 | 조용한 에이전트의 경우 빈 배열 |
GET /img/<hash> | 콘텐츠 해시별 하나의 이미지, immutable | 뷰어당 이미지당 한 번, 이후 캐시됨 |
여기서 무언가를 변경하기 전에 알아야 할 두 가지 결과:
since는 트리 자체의count를 인덱싱합니다. 트리 행은 로그가 렌더링되지 않았더라도 에이전트의 실제 이벤트 수를 전달하며, 클라이언트가since로 반환하는 숫자입니다. 둘 다 동일한 트랜스크립트 파싱에서 나오며, 그래야 합니다: 다른 곳에서 하나를 파생시키면 클라이언트가 조용히 이벤트를 건너뛰거나 반복하게 되어, 로그가 미묘하게 잘못된 것 외에는 볼 수 있는 것이 없습니다.GET /img/<hash>는 콘텐츠 주소 지정이므로 응답은 영원히 캐시됩니다. 이것이 알 수 없는 해시가 200이 아닌 404인 이유입니다 - 빈 200은 해당 뷰어에게 영구적으로 깨진 이미지이며, 새로고침으로도 해결되지 않습니다.
이 분할이 손실되어도 아무것도 빨간색이 되지 않습니다. 모든 로그를 GET /state에 넣거나,
모든 스냅샷에 이미지를 인라인으로 넣으면 트리는 정확하고, 레이블은 정확하며, 페이지가 렌더링되지만,
수백 메가바이트가 됩니다. 크기는 보호 대상 속성입니다;
serve.test.mjs (6-9)의 크기 검증 항목만이 회귀를 감지할 수 있으며, 각각은
PAIR입니다 - 여기에는 없고 저기에는 있음 - "X가 없다"는 것은 그 외의 모든 잘못된 이유로 참이기 때문입니다.
저장된 페이지가 가진 것과 가지지 않은 것
GET /는 하나의 독립형 HTML 문서입니다: 인라인 CSS 및 JS, 외부 자산 없음,
서버 없이도 렌더링되는 내장 스냅샷 포함. 내장된 것은 전체 트리와 루트 세션의 로그이며,
해당 콘텐츠의 이미지는 data URI로 포함됩니다.
내장되지 않은 것: 다른 모든 에이전트의 로그. 이들은 문서에 포함되지 않으며,
선택 시 /agent/<id>에서 도착합니다. 따라서 디스크에 저장된 페이지나 서버가 종료된 페이지는
완전한 그래프와 루트 세션의 로그를 표시하지만, 하위 에이전트 블록을 클릭하면 아무것도 표시되지 않습니다.
해당 로그 내의 이미지도 마찬가지입니다: /img/<hash>에서 렌더링되며 저장된 파일 뒤에는 서버가 없습니다.
모든 로그를 하나의 파일에 고정 기록하려면 build.mjs를 사용하세요 - 정적 빌드는
모든 에이전트의 로그와 모든 이미지를 인라인으로 포함합니다. 기념 스냅샷은 가져올 것이 없기 때문입니다.
snapshot()에 모든 로그를 요청하는 유일한 호출자입니다 (logsFor: null).
세션을 추적하는 방법
서버는 오래 지속됩니다; Claude 세션은 그렇지 않습니다: 세션 종료, 업데이트, 유닛 재시작 모두 새 세션 ID를 생성합니다. 따라서 서버는 시작 시 하나를 확인하는 대신 매 스냅샷마다 가장 최근에 활성화된 세션을 다시 선택하며, 아직 존재하지 않으면 "세션을 기다리는 중..." 페이지를 제공합니다 - 부팅 시 정상 상태입니다. "가장 최근에 활성화된"에는 ROOT 트랜스크립트도 포함되므로, 아직 위임하지 않은 갓 시작된 세션은 즉시 추적되어 루트 전용 트리로 렌더링되며, 첫 번째 위임부터 스폰 트리가 성장합니다. 새 세션이 시작될 때 아무것도 재시작할 필요가 없습니다; 페이지는 ID 변경을 감지하고 로그를 재구성합니다.
--session=<id>는 대신 하나의 세션을 고정합니다 (디버깅을 위한 고정된 보기).
config 디렉토리 아래의 모든 프로젝트 슬러그가 스캔됩니다, 하나가 아닙니다. 세션의 트랜스크립트는 해당 CWD의 프로젝트 슬러그 아래에 저장되므로, git 워크트리에서 작업하는 세션은 WORKTREE의 슬러그 아래에 기록됩니다; 하나의 슬러그를 고정하면 모든 워크트리 세션이 보이지 않게 되지만 페이지는 여전히 완벽하게 렌더링됩니다.
페이지가 표시하는 에이전트
루트 블록은 config 디렉토리가 속한 fleet 멤버로 레이블이 지정되며,
config.json의 fleet.by_config_dir을 통해 확인됩니다. 키는 원시 기본 이름이 아닌 파생되며,
파생 과정이 읽어볼 가치가 있는 부분입니다:
format.mjs의fleetKey()는 config 디렉토리의 기본 이름을 가져오지만, 해당 기본 이름이 리터럴.claude인 경우 대신 PARENT 디렉토리의 기본 이름을 사용합니다. 각 에이전트에 홈 디렉토리를 제공하는 fleet은 모든 config 디렉토리를<home>/<agent>/.claude에 배치하므로, 기본 이름으로 키를 지정하면 모두 하나의 항목에 충돌하여 모든 에이전트의 페이지가 해당 항목이 명명하는 에이전트로 레이블이 지정됩니다.
이 실패는 조용합니다: 페이지는 완전히 렌더링되지만 잘못된 이름을 표시하며,
두 개의 페이지를 연 뷰어는 구분할 수 없고 구분할 수 없다는 사실을 알지 못합니다.
리터럴 .claude 항목은 폴백으로 존중되므로, .claude로 키가 지정된 단일 에이전트 설치는
레이블을 유지합니다; 두 개 이상의 에이전트를 구분할 수 없으며 두 개 이상 존재하는 경우 사용해서는 안 됩니다.
동일한 파생 키는 동일한 이유로 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는 사용법, 확인된 config 경로, 트랜스크립트 루트 및 기본 포트를 출력하며 아무것도 바인딩하지 않음.--port=0은 임시 포트를 사용하며 배너는 실제로 얻은 포트를 출력합니다.format.mjs- 공유 포맷터: 트랜스크립트 파싱, 스폰 트리 레이아웃, 이벤트별 HTML, 테마 CSS, 그리고 로컬 값이 읽히는 유일한 장소 (fleet where가 명명하는config.json또는WEB_HIERARCHY_CONFIG가 설정된 경우 그 내용을 통해cfg()).build.mjs- 선택적 STATIC 스냅샷 (node build.mjs <out.html>): 현재 상태를 하나의 자체 포함 파일로 빌드하여 고정되고 공유 가능한 기록을 제공합니다. 라이브 보기가 아닌 기념 스냅샷에 사용하세요.identities.json+avatars/- 표시 이름 명단,identities.file이 다른 파일을 명명하지 않는 한 사용됨.serve.test.mjs+fixtures/- 테스트: 임시 포트에서 실제 서버를 시작하고, 합성 트랜스크립트 트리를 대상으로, 트리, 모든 경로(로그 없는 트리, 한 에이전트의 로그만,?since는 새 것만 전송, 해시별로 바이트 단위로 제공되는 이미지)를 단언한 다음, 하나의 하위 에이전트 트랜스크립트를 DELETES하고 트리와 에이전트별 경로 모두에서 사라지도록 요구합니다. 헤더는 각 검증 항목이 있는 이유, 각 크기 검증 항목이 어떤 쌍의 절반인지, 그리고 왜 음성 제어가 하중을 지탱하는 부분인지 명시합니다. 검증 항목 11은GET /state?view=active가 기본 보기가 그리는 중지된 블록을 삭제하도록 요구합니다.view.test.mjs- 자체적인 "활성 전용" 규칙 (filterView()over a 합성 트리): 활성 블록, 그 위의 블록 및 루트, 그 옆의 중지된 형제 및 자식은 제거되며, "전체"는 모든 행을 변경 없이 반환합니다.format.test.mjs- config를 찾고 읽는 방법: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, harness가agent-<id>.meta.json에 복사합니다. 없을 때의 폴백: 에이전트 작업 프롬프트의 첫 줄. 설명은 줄바꿈되며 블록은 맞게 커집니다. - 모든 블록의 마지막 로그 줄, 호버 툴팁으로. 수백 개의 에이전트가 있을 때 동일한 유형과 동일한 설명을 가진 두 블록을 구분하는 유일한 것이며, 활성 에이전트의 경우 현재 수행 중인 작업을 나타냅니다. 이는 트리에 포함됩니다. 충분히 저렴한 로그 콘텐츠이기 때문입니다: 일반 텍스트, 제한됨, 절대 HTML 아님 - 마크업을 렌더링하는 순간 이미지와 확장기를 모든 스냅샷으로 가져옵니다.
- 모든 블록의 모델.
parseTranscript는 트랜스크립트의 보조 턴에서 모델을 읽습니다 (claude-접두사 제거됨); 에이전트가 아직 응답하지 않은 경우.meta.json의 대략적인 별칭이 대신 사용됩니다. 둘 다 없는 노드는 플레이스홀더 대신 줄을 렌더링하지 않습니다. - 아바타가 있는 명명된 에이전트. 각 블록은 이름, 아바타, agentType,
설명, 모델 및 시간 범위를 표시하므로, 동일한 유형의 다섯 동시 에이전트를 한눈에 구분할 수 있습니다.
- 이름은 표시 레이블일 뿐입니다:
identities.file의 명단에서 (agentType, 해당 유형 내 스폰 순서)의 순수 함수로 할당되며, 매 새로고침마다 트랜스크립트에서 다시 계산됩니다. 아무데도 저장되지 않으며, 에이전트는 자신의 이름을 알지 못합니다 - harness가 트랜스크립트를 소유하고 이 포맷터는 읽기 전용 관찰자입니다. - 스폰 순서, 에이전트 ID의 해시가 아닙니다: N개의 이름과 N개의 에이전트가 있는 경우 해시는 비둘기집 원리로 충돌합니다; 스폰 순서는 충돌할 수 없습니다.
names순서는 하중을 지탱합니다 - 추가 전용입니다. 재정렬하거나 중간에 삽입하면 과거 모든 세션의 그래프가 조용히 이름이 변경됩니다.avatar= 역할의 실루엣;portraits= 이름에 대한 선택적 맞춤형 아트. 아바타는 agentType을 식별하고, 초상화는 개별 에이전트를 식별합니다. 초상화가 없는 이름은 아바타를 사용합니다 - 각 인스턴스는 고유합니다. 아바타와 초상화는identities.json또는avatars/디렉토리에서format.mjs의roleAvatar()와lookupPortrait()를 통해 발견됩니다.
- 이름은 표시 레이블일 뿐입니다:
사용자가 보는 것
터미널을 볼 수 없는 인간 뷰어는 브라우저에서 이 페이지를 열고, 에이전트가 작업을 위임하고 완료함에 따라 트리가 성장하고 수축하는 것을 지켜봅니다. 뷰어는 블록을 클릭하여 로그를 검사하거나, 활성 에이전트만 표시하도록 전환하거나, 나중에 분석하기 위해 페이지를 저장할 수 있습니다.
페이지는 10초마다 업데이트되므로 항상 최신 상태를 반영합니다. 새 세션이 시작되면 페이지가 자동으로 전환되며 중단이 없습니다. 서버가 재시작되면 페이지가 다시 연결되고 업데이트를 재개합니다.
문제 해결
에이전트가 페이지에 표시되지 않는 경우:
- 서버가 실행 중인지 확인합니다:
installation/check - config 디렉토리가 올바른지 확인합니다:
fleet where web-hierarchy-server - config.json이 유효한지 확인합니다
- 서버 로그에 오류가 있는지 확인합니다
로그가 로드되지 않는 경우:
- 트랜스크립트가 존재하는지 확인합니다
- 파일 권한이 올바른지 확인합니다
- 서버가 트랜스크립트를 읽을 수 있는지 확인합니다