diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a101e54..657d6a7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -54,6 +54,16 @@ "name": "hermes", "source": "./hermes-gateway-plugin", "description": "Interact with Hermes Agent via local or SSH-tunneled connection" + }, + { + "name": "split-work-plugin", + "source": "./split-work-plugin", + "description": "Split current project work into parallel-safe task groups with worktree branch names and structured starting prompts" + }, + { + "name": "browser-walkthrough-plugin", + "source": "./browser-walkthrough-plugin", + "description": "Headed browser walkthrough — step-by-step interactive flow for iframe/security-heavy sites. Requires playwright-cli skill" } ] } diff --git a/browser-walkthrough-plugin/.claude-plugin/plugin.json b/browser-walkthrough-plugin/.claude-plugin/plugin.json new file mode 100644 index 0000000..8fdbe26 --- /dev/null +++ b/browser-walkthrough-plugin/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "browser-walkthrough-plugin", + "description": "Headed browser walkthrough — step-by-step interactive flow with the user. Pairs with playwright-cli skill for iframe-heavy/security-program-heavy sites (홈택스, 정부24, banking).", + "version": "1.0.0", + "author": { + "name": "Stefan Cho" + } +} diff --git a/browser-walkthrough-plugin/README.md b/browser-walkthrough-plugin/README.md new file mode 100644 index 0000000..7072f5b --- /dev/null +++ b/browser-walkthrough-plugin/README.md @@ -0,0 +1,39 @@ +# browser-walkthrough-plugin + +playwright-cli **headed 모드**로 브라우저를 같이 보면서 한 스텝씩 대화형으로 진행하는 워크플로우. 홈택스·정부24·은행·쇼핑몰처럼 iframe·보안프로그램·팝업이 많은 사이트를 사용자와 함께 진행할 때 사용. + +## 전제 조건 (사전 설치 필요) + +이 플러그인은 별도 `playwright-cli` 스킬에 의존한다. 본 플러그인에 묶지 않은 이유: playwright-cli는 외부에서 활발히 갱신되고 다른 플러그인(impeccable, frontend-design 등)도 동일 스킬을 가져오므로, 묶으면 본가 업데이트가 끊긴다. + +설치 옵션: +- 가장 간단: 외부 플러그인(예: `frontend-design`, `impeccable`) 설치 시 `playwright-cli` 스킬이 함께 따라옴 +- 또는 user-level에 직접 두기: `~/.claude/skills/playwright-cli/` + +`playwright-cli`가 사용 가능한 상태인지 먼저 확인 후 본 플러그인을 사용하라. + +## 사용법 + +자연어 트리거: +- "브라우저 보면서 같이 진행" +- "홈택스 같이 / 정부24 같이" +- "step-by-step browser" / "한 스텝씩" + +## 대화 프로토콜 (요약) + +| 입력 | 행동 | +|---|---| +| `다음`, `next`, `진행`, `ok` | 직전 제안 액션 실행 → snapshot → 다음 스텝 | +| `뒤로` | go-back | +| `잠깐`, `wait` | 액션 금지, 대기 (사용자 수동 조작 중) | +| 구체적 값 | 직전 필드에 fill | +| 질문 | 답변만 하고 대기 | +| `ok 제출`, `결제 진행` | 위험 액션 게이트 통과 | +| `완료`, `끝` | 후속 액션 아이템 요약 | + +## Install + +```bash +/plugin marketplace add devstefancho/claude-plugins +/plugin install browser-walkthrough-plugin@devstefancho-claude-plugins +``` diff --git a/browser-walkthrough-plugin/skills/browser-walkthrough/SKILL.md b/browser-walkthrough-plugin/skills/browser-walkthrough/SKILL.md new file mode 100644 index 0000000..10cad57 --- /dev/null +++ b/browser-walkthrough-plugin/skills/browser-walkthrough/SKILL.md @@ -0,0 +1,219 @@ +--- +name: browser-walkthrough +description: playwright-cli headed 모드로 브라우저를 같이 보면서 한 스텝씩 대화형으로 진행하는 워크플로우. 사용자가 "다음"이라고 하면 다음 스텝 실행, 질문하면 답만 하고 대기, 최종 제출 같은 위험 액션은 명시적 키워드 필요. 홈택스·정부24·은행·쇼핑몰처럼 iframe·보안프로그램·팝업이 많은 사이트를 사용자와 함께 진행할 때 사용. Trigger: "브라우저 보면서 같이 진행", "headed 모드로 같이", "단계별로 진행해줘", "홈택스 같이", "정부24 같이", "step-by-step browser", "walkthrough 모드", "한 스텝씩", "한 단계씩 진행". +allowed-tools: Bash +--- + +# Browser Walkthrough (headed 모드 대화형 진행) + +사용자가 열어둔 headed 브라우저에 **attach**해서 같은 화면을 보며 한 스텝씩 진행한다. +명령어 전체 레퍼런스는 `playwright-cli` skill 참조. 이 skill의 핵심은 **대화 프로토콜**이다. + +> **사전 조건**: `playwright-cli` 스킬이 사용 가능해야 한다 (외부 플러그인 또는 user-level 설치). 본 플러그인은 의존만 표시하고 묶지 않는다 — 본가 업데이트가 끊기지 않게 하려는 의도. 자세한 내용은 README 참조. + +## 사용자 응답 해석 (최우선) + +| 입력 | 행동 | +|---|---| +| `다음` / `next` / `진행` / `ok` / `응` | 직전에 제안한 액션 실행 → snapshot → 다음 스텝 요약·대기 | +| `뒤로` | `go-back` | +| `스킵` / `건너뛰기` | 현재 스텝 건너뛰고 다음 | +| `잠깐` / `멈춰` / `wait` / `대기` | 아무 액션도 하지 말고 대기. 사용자가 수동 조작 중 (인증서 비번, 보안프로그램 설치, 텍스트 마스킹 등) | +| 구체적 값 (`스테판초 랩스`, `010-...`, `10`) | 직전에 물어본 필드에 `fill` | +| 질문 (`이거 뭐임?`, `~~는 뭐가 유리?`) | **액션 금지.** 답변만 하고 다시 대기 | +| `ok 제출` / `결제 진행` / `최종 확인` | 위험 액션 실행 (아래 "위험 액션 게이트") | +| `완료` / `끝` | 후속 액션 아이템 요약 (기한·체크리스트) | + +## Step Loop + +매 스텝 반복: + +1. `playwright-cli -s= snapshot` — 현재 화면의 DOM(yaml, ref 포함) 확보 +2. 2-5줄 요약 — 무슨 페이지이고 어떤 선택지·필드가 있는지 +3. 다음 액션 제안 or 필요한 입력값 질문 +4. 사용자 응답 대기 → 위 표대로 해석 + +snapshot이 비거나 ref가 안 잡히면 iframe 가능성 → `references/iframe-handling.md` 참고. + +## Bootstrap + +```bash +# 기존 세션 확인 +playwright-cli list + +# 없으면 headed 크롬으로 새 세션 (세션 이름은 도메인 기반) +playwright-cli -s= open --browser=chrome --persistent --headed + +# 이미 열린 브라우저가 attach 가능하면 +playwright-cli -s= attach +``` + +세션 이름은 작업 도메인 (`hometax`, `gov24`, `coupang`). 이후 **모든 명령에 `-s=` 필수.** + +## 위험 액션 게이트 (필수) + +최종 제출, 결제, 삭제, 취소 등 **되돌릴 수 없는 액션**은 `다음`만으로 실행하지 않는다. + +실행 전에: +1. **체크포인트 요약 표**로 지금까지 입력한 핵심 값 재표시 (금액·날짜·상호·접수번호 등) +2. `ok 제출`, `결제 진행`, `최종 확인` 중 어떤 키워드가 필요한지 명시 +3. 사용자가 그 키워드를 말할 때까지 대기 + +## 중간 체크포인트 + +긴 폼(5-6단계 이상)에서는 페이지 전환 직전에 지금까지 채운 값을 짧은 표로 재요약해 스텝 드리프트 방지. + +## 민감값 처리 + +- 사용자가 브라우저 확장으로 주민번호·계좌·비번 등을 **마스킹 중**일 수 있다. screenshot에 마스킹된 모습이 보여도 헷갈리지 말 것. +- 주민번호·인증서 비밀번호·계좌번호·카드번호는 **응답에 그대로 에코 금지.** +- 인증서 비번 입력 같은 단계는 사용자가 직접 하도록 요청하고 `잠깐` 대기. + +## 한국 사이트 특이사항 + +- 홈택스·금융사: iframe 안에 실제 컨텐츠 → `references/iframe-handling.md` +- IPINSIDE / Veraport / MagicLine: 보안프로그램 설치 게이트. playwright에서 우회 불가, 사용자 수동 설치 후 새로고침 +- 다운로드: playwright가 `/private/var/folders/**/playwright-artifacts-*/`로 가로챔. 필요시 `cp`로 `~/Downloads`에 복사 → `references/downloads.md` +- 공동인증서 팝업: 사용자가 직접 비번 입력. Claude는 `잠깐` 모드 + +## 안내(Guide) 모드 — 사용자가 직접 클릭하게 하기 + +기본은 Claude가 `click`/`fill`한다. 다만 다음 상황에선 **대상을 강조만 하고 사용자가 직접 조작**하는 게 낫다: + +- **인증/민감정보 입력**: 카드번호, 주민번호, OTP 등 — Claude가 값을 몰라야 안전 +- **학습 목적**: 사용자가 다음부터 혼자 할 수 있도록 경로를 체감시켜야 할 때 +- **최종 제출·결제 버튼**: 위험 액션 게이트와 결합. highlight로 강조 → 사용자가 `ok 제출` 키워드 말하면 Claude가 click + +### 사용법 + +```bash +scripts/guide-launcher.sh "" "<ref|heading|desc>" [more...] +``` + +**모드**: +- `seq` — 순차 스텝, 스텝별 다른 색상 (핑크/보라/청록/주황/파랑/빨강/민트/자주 순환) +- `opt` — 택1 옵션, 주황색 + 알파벳 라벨 (A/B/C) +- `one` — 단일 타겟, 핑크 + 👉 + +**Spec 포맷**: `ref|heading|desc` (heading/desc 생략 가능, desc 안의 `\n`은 줄바꿈) + +**환경변수**: `CC_HL_SESSION` (기본값 `hometax`) — 다른 playwright-cli 세션이면 지정 +```bash +CC_HL_SESSION=gov24 scripts/guide-launcher.sh seq "..." "..." +``` + +### 렌더 구성 + +- 각 대상 요소에 색상 outline + 좌상단 번호/알파벳 배지 (스크롤·resize·DOM 변경 자동 추적) +- **우측 상단 고정 사이드 패널**에 헤딩·설명 리스트 +- 패널 항목 클릭 → 해당 요소로 scrollIntoView + 3회 깜빡임 +- 패널 "×" 닫기 → 전체 outline·배지·패널 제거 + +### 예시 + +```bash +scripts/guide-launcher.sh seq "카드 등록 4단계" \ + "e1033|카드사 선택|드롭다운에서 본인 카드사" \ + "e1035|카드번호 16자리|4자리씩 4칸" \ + "e1067|휴대전화|가운데·뒤 4자리" \ + "e1082|등록접수하기|⚠ 누르기 전 확인" +``` + +### 판단 기준 — Claude 직접 click vs highlight 가이드 + +| 상황 | 권장 방식 | +|---|---| +| 단순 폼 (제목·내용·날짜 등 비민감) | Claude `fill` | +| 민감값 (아이디/비번, 카드번호, 전화번호) | `highlight` → 사용자 직접 | +| 체크박스·라디오 선택 | Claude `click` (확인 후) | +| 최종 제출·결제·삭제 | `highlight` → `ok 제출`에 Claude가 최종 click | +| 인증서 로그인·OTP | 기본 대기 (`잠깐`) | + +## Dialog / Tab + +```bash +playwright-cli -s=<name> dialog-accept +playwright-cli -s=<name> dialog-dismiss +playwright-cli -s=<name> tab-list +playwright-cli -s=<name> tab-select 2 +``` +snapshot에 `dialog` 키워드 있으면 먼저 accept/dismiss. 팝업이 떴으면 tab-list부터. + +## 폼 일괄 입력 + +여러 필드 확정되면 한 번에: +```bash +playwright-cli -s=<name> fill e133 "0" && \ +playwright-cli -s=<name> fill e138 "10" && \ +playwright-cli -s=<name> click e174 +``` + +## 스크린샷 해상도 (중요) + +Claude vision encoder는 긴 변 **1568px**까지만 손실 없이 처리한다 (그 이상은 자동 다운스케일되어 픽셀 낭비 + 응답 지연). 토큰 ≈ (w×h)/750. + +**원칙**: screenshot 찍고 바로 `sips`로 리사이즈한 뒤 Read. + +```bash +# 1. 스크린샷 +playwright-cli -s=<name> screenshot --filename=step.png +# 2. 긴 변 1568px로 다운스케일 (원본보다 작으면 그대로 둠) +sips -Z 1568 .playwright-cli/step.png --out .playwright-cli/step-small.png +# 3. Read로 확인 +``` + +한 줄 체이닝: +```bash +playwright-cli -s=<name> screenshot --filename=s.png && sips -Z 1568 .playwright-cli/s.png --out .playwright-cli/s-small.png >/dev/null +``` + +예외: 폰트 작거나 PDF 페이지 읽기 등 **해상도 낮아 못 읽을 때만** 1568 상향(2000~2500). 대부분 1568로 충분. + +snapshot(yaml)은 토큰 기준이라 해상도 무관 — 가능하면 screenshot보다 snapshot 먼저. + +## PDF 뷰어 + +playwright로 페이지 넘기기 어려움 → 사용자에게 "n페이지로 넘겨줘" 요청 → `screenshot` 으로 읽음 (위 리사이즈 적용). + +## 세션 종료 + +```bash +playwright-cli -s=<name> close # 창만 닫기 +playwright-cli -s=<name> delete-data # 퍼시스턴트 프로필까지 삭제 +``` + +사용자가 `완료`라고 하면 후속 액션 아이템을 **기한 포함 체크리스트**로 요약. (세션에서 효과적이었던 패턴) + +## 예시 흐름 + +``` +User: 홈택스 개인사업자등록 같이 진행해줘 +Claude: playwright-cli -s=hometax list → snapshot → "로그인 페이지. 로그인해달라" → 대기 + +User: 로그인했음 +Claude: snapshot → "메인 진입. '증명·등록·신청' hover 하면 서브메뉴 열림. 진행?" → 대기 + +User: 다음 +Claude: hover → snapshot → "서브메뉴 열림. '개인 사업자등록 신청' 4가지 중 어떤 거?" → 대기 + +User: 기본 +Claude: click → dialog-accept (보안 alert) → snapshot → "상호 입력 필드. 뭐로?" → 대기 + +User: 스테판초 랩스 +Claude: fill → snapshot → 다음 필드 ... + +... (여러 스텝 후) + +Claude: "최종 제출 직전. [체크포인트 표]. 'ok 제출' 답하면 접수." + +User: ok 제출 +Claude: click → "접수 완료 (번호: ...)" + +User: 완료 +Claude: "[후속 체크리스트: 4/21 건보 확인, 4/22 등록증 출력, ...]" +``` + +## 참고 문서 + +- `references/iframe-handling.md` — iframe 안 요소 찾기/클릭 패턴 +- `references/downloads.md` — playwright 다운로드 가로채기 대응 diff --git a/browser-walkthrough-plugin/skills/browser-walkthrough/references/downloads.md b/browser-walkthrough-plugin/skills/browser-walkthrough/references/downloads.md new file mode 100644 index 0000000..4367d8b --- /dev/null +++ b/browser-walkthrough-plugin/skills/browser-walkthrough/references/downloads.md @@ -0,0 +1,60 @@ +# 다운로드 처리 + +playwright headed 모드에서 브라우저가 파일을 다운로드하면, playwright가 이를 **임시폴더에 UUID 이름으로** 저장한다 (Chrome이 `~/Downloads`로 저장하는 게 아니다). + +## 위치 찾기 + +```bash +ls -la /private/var/folders/**/playwright-artifacts-*/ 2>/dev/null +# 또는 최근 10분내 생성된 파일 +find ~/Library/Caches/ms-playwright /private/var/folders -type f -mmin -10 2>/dev/null | head -20 +``` + +파일은 확장자 없이 UUID 형식으로 저장된다. + +## 파일 타입 확인 + +```bash +file /private/var/folders/.../playwright-artifacts-.../UUID +# 예: xar archive → .pkg +# 예: PDF document, version 1.4 → .pdf +# 예: Zip archive → .zip +``` + +## Downloads로 복사 + rename + +```bash +cp /private/var/folders/.../playwright-artifacts-.../<UUID> ~/Downloads/<의미있는이름>.<확장자> +``` + +## Chrome 내부 다운로드 목록 조회 + +`chrome://downloads/` 탭을 열어 shadow DOM으로 조회: + +```bash +playwright-cli -s=<name> tab-new chrome://downloads/ +playwright-cli -s=<name> eval "(() => { + const m = document.querySelector('downloads-manager'); + const items = m?.shadowRoot?.querySelectorAll('downloads-item'); + if (!items) return 'no items'; + return Array.from(items).slice(0, 5).map(it => { + const s = it.shadowRoot; + return { + name: s?.querySelector('#name')?.textContent?.trim(), + url: s?.querySelector('#url')?.href, + status: s?.querySelector('.description')?.textContent?.trim() + }; + }); +})()" +``` + +원래 탭으로 복귀: +```bash +playwright-cli -s=<name> tab-select 0 +``` + +## macOS 설치 패키지(.pkg) + +사용자 동의 없이 `installer` / `open` 으로 자동 실행하지 않는다. 복사 후 Finder에서 더블클릭하도록 안내: + +> "`~/Downloads/파일명.pkg` 를 Finder에서 더블클릭 → macOS Installer 마법사 따라가기. 설치 완료 후 Chrome 탭에서 Cmd+R 새로고침하고 알려줘." diff --git a/browser-walkthrough-plugin/skills/browser-walkthrough/references/iframe-handling.md b/browser-walkthrough-plugin/skills/browser-walkthrough/references/iframe-handling.md new file mode 100644 index 0000000..df819d5 --- /dev/null +++ b/browser-walkthrough-plugin/skills/browser-walkthrough/references/iframe-handling.md @@ -0,0 +1,73 @@ +# iframe 안 요소 다루기 + +홈택스·정부24·은행 등 한국 사이트는 실제 컨텐츠가 iframe 안에 있어 `snapshot`에 ref가 안 잡힐 수 있다. + +## iframe 내부 버튼/링크 스캔 + +```bash +playwright-cli -s=<name> eval "(() => { + const iframe = document.querySelector('iframe'); + const doc = iframe && iframe.contentDocument; + if (!doc) return 'no iframe doc'; + const els = Array.from(doc.querySelectorAll('button, input[type=button], a')); + return els.map(b => ({ + tag: b.tagName, + text: (b.textContent || b.value || '').trim().slice(0, 50), + href: (b.href || '').slice(0, 80), + onclick: (b.getAttribute('onclick') || '').slice(0, 80) + })).filter(b => b.text); +})()" +``` + +키워드 필터링 (확인/닫기/다음/진행 등): + +```bash +playwright-cli -s=<name> eval "(() => { + const iframe = document.querySelector('iframe'); + const doc = iframe?.contentDocument; + if (!doc) return 'no doc'; + const els = Array.from(doc.querySelectorAll('button, input[type=button], a')); + return els + .map(b => ({ text: (b.textContent || b.value || '').trim(), el: b })) + .filter(b => /확인|닫기|다음|진행|계속|시작|바로가기|메인/.test(b.text)) + .map(b => b.text); +})()" +``` + +## iframe 내부 버튼 클릭 + +ref가 안 잡히면 텍스트로 매치해서 직접 click: + +```bash +playwright-cli -s=<name> eval "(() => { + const iframe = document.querySelector('iframe'); + const doc = iframe?.contentDocument; + const btn = doc && Array.from(doc.querySelectorAll('input[type=button], button')) + .find(b => (b.value || b.textContent || '').trim() === '닫기'); + if (!btn) return 'no btn'; + btn.click(); + return 'clicked'; +})()" +``` + +## iframe src 확인 (어떤 페이지인지) + +```bash +playwright-cli -s=<name> eval "(() => { + const iframe = document.querySelector('iframe'); + return { src: iframe?.src, name: iframe?.name, title: iframe?.contentDocument?.title }; +})()" +``` + +src의 쿼리스트링이 "installList=IPINSIDE-NX" 같은 보안프로그램 게이트를 드러낼 수 있다. + +## snapshot을 파일로 저장해 grep + +ref 많은 페이지는 파일로 저장 후 keyword grep이 빠르다: + +```bash +playwright-cli -s=<name> snapshot --filename=current.yml +grep -n "사업자등록\|신청\|확인" .playwright-cli/current.yml | head +``` + +snapshot 파일 기본 경로: `.playwright-cli/page-<timestamp>.yml` (미지정 시). diff --git a/browser-walkthrough-plugin/skills/browser-walkthrough/scripts/guide-launcher.sh b/browser-walkthrough-plugin/skills/browser-walkthrough/scripts/guide-launcher.sh new file mode 100755 index 0000000..65dfb24 --- /dev/null +++ b/browser-walkthrough-plugin/skills/browser-walkthrough/scripts/guide-launcher.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# guide-launcher.sh — inject a guide overlay into a playwright-cli browser session. +# Parses CLI spec, tags target elements, and evaluates guide-overlay.js (same directory). +# +# Usage: +# ./guide-launcher.sh <mode> "<title>" "<ref|heading|desc>" [more...] +# +# Modes: +# seq → sequential steps, per-step color (1/2/3/...) +# opt → pick-one options, single orange, letters (A/B/C) +# one → single target, pink (👉) +# +# Spec: "ref|heading|desc" (heading/desc optional, \n → newline in panel) +# +# Elements: +# - outline on target (scroll-follows automatically) +# - small round badge next to each target (position:fixed, tracks via rAF) +# - fixed side panel on the right with all step descriptions +# +set -e +cd "$(dirname "$0")" + +MODE="$1" +TITLE="$2" +shift 2 || { echo "Usage: $0 <mode> <title> <ref|heading|desc> [...]"; exit 1; } + +case "$MODE" in + seq|opt|one) ;; + *) echo "mode must be: seq | opt | one"; exit 1 ;; +esac + +[ $# -eq 0 ] && { echo "at least one target required"; exit 1; } + +SESSION="${CC_HL_SESSION:-hometax}" + +# 1) clear any previous data-cc-step tags first (idempotent) +playwright-cli -s="$SESSION" eval "() => { + document.querySelectorAll('[data-cc-step]').forEach(x => x.removeAttribute('data-cc-step')); +}" > /dev/null 2>&1 || true + +# 2) tag each target via playwright ref (unique per session) +i=1 +for SPEC in "$@"; do + REF="${SPEC%%|*}" + if [ -z "$REF" ]; then + echo "skip empty ref" + i=$((i + 1)) + continue + fi + playwright-cli -s="$SESSION" eval "el => el.setAttribute('data-cc-step', '$i')" "$REF" > /dev/null + i=$((i + 1)) +done + +# 3) build specs JSON from heading|desc +SPECS_JSON=$(python3 -c ' +import json, sys +specs = [] +for raw in sys.argv[1:]: + parts = raw.split("|", 2) + specs.append({ + "heading": parts[1] if len(parts) > 1 else "", + "desc": parts[2] if len(parts) > 2 else "", + }) +print(json.dumps(specs, ensure_ascii=False)) +' "$@") + +MODE_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1], ensure_ascii=False))' "$MODE") +TITLE_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1], ensure_ascii=False))' "$TITLE") + +# 4) substitute placeholders in the JS template +JS_CODE=$(python3 -c ' +import sys +with open(sys.argv[1], "r", encoding="utf-8") as f: + tpl = f.read() +tpl = tpl.replace("__MODE__", sys.argv[2]) +tpl = tpl.replace("__TITLE__", sys.argv[3]) +tpl = tpl.replace("__SPECS__", sys.argv[4]) +sys.stdout.write(tpl) +' "$(dirname "$0")/guide-overlay.js" "$MODE_JSON" "$TITLE_JSON" "$SPECS_JSON") + +# 5) run +playwright-cli -s="$SESSION" eval "$JS_CODE" > /dev/null + +echo "✓ [$MODE] $# target(s) highlighted · side panel (top-right) + scroll-tracking badges" diff --git a/browser-walkthrough-plugin/skills/browser-walkthrough/scripts/guide-overlay.js b/browser-walkthrough-plugin/skills/browser-walkthrough/scripts/guide-overlay.js new file mode 100644 index 0000000..6b02d14 --- /dev/null +++ b/browser-walkthrough-plugin/skills/browser-walkthrough/scripts/guide-overlay.js @@ -0,0 +1,144 @@ +() => { + const MODE = __MODE__; + const TITLE = __TITLE__; + const SPECS = __SPECS__; + + const SEQ_COLORS = ['#ff2d95', '#8b5cf6', '#10b981', '#f59e0b', '#3b82f6', '#ef4444', '#14b8a6', '#d946ef']; + const OPT_COLOR = '#ff8800'; + const ONE_COLOR = '#ff2d95'; + const colorFor = (i) => MODE === 'seq' ? SEQ_COLORS[i % SEQ_COLORS.length] : (MODE === 'opt' ? OPT_COLOR : ONE_COLOR); + const labelFor = (i) => MODE === 'seq' ? String(i + 1) : (MODE === 'opt' ? String.fromCharCode(65 + i) : '👉'); + + if (window.__ccHl && typeof window.__ccHl.cleanup === 'function') { + try { window.__ccHl.cleanup(); } catch (e) {} + } + document.querySelectorAll('[data-cc-hl]').forEach(x => { + x.style.outline = ''; + x.style.outlineOffset = ''; + x.style.boxShadow = ''; + x.removeAttribute('data-cc-hl'); + }); + document.querySelectorAll('.cc-hl-badge, .cc-hl-card, .cc-hl-banner').forEach(x => x.remove()); + ['cc-hl-style', 'cc-hl-panel'].forEach(id => { const e = document.getElementById(id); if (e) e.remove(); }); + + const style = document.createElement('style'); + style.id = 'cc-hl-style'; + style.textContent = [ + '.cc-hl-badge { position:fixed; z-index:999999; min-width:28px; height:28px; padding:0 8px; border-radius:14px; color:#fff; font:bold 13px/28px -apple-system,system-ui,sans-serif; text-align:center; box-shadow:0 2px 8px rgba(0,0,0,0.35); pointer-events:none; white-space:nowrap; transition:transform .15s; }', + '.cc-hl-panel { position:fixed; z-index:999998; top:72px; right:16px; width:320px; max-height:calc(100vh - 92px); display:flex; flex-direction:column; background:#fff; border-radius:12px; box-shadow:0 10px 32px rgba(0,0,0,0.22); font:13px/1.4 -apple-system,system-ui,sans-serif; color:#222; overflow:hidden; }', + '.cc-hl-panel.collapsed .cc-hl-body { display:none; }', + '.cc-hl-panel-head { padding:10px 12px; border-bottom:1px solid #eee; display:flex; gap:8px; align-items:center; font-weight:700; flex-shrink:0; background:#fafafa; }', + '.cc-hl-panel-head .cc-title { flex:1; font-size:14px; color:#222; }', + '.cc-hl-panel-head button { background:transparent; border:0; color:#666; cursor:pointer; font-size:15px; padding:2px 6px; line-height:1; }', + '.cc-hl-panel-head button:hover { color:#000; }', + '.cc-hl-body { padding:4px 12px 12px; overflow:auto; }', + '.cc-hl-item { display:flex; gap:10px; padding:10px 0; cursor:pointer; border-bottom:1px dashed #eee; }', + '.cc-hl-item:last-child { border-bottom:0; }', + '.cc-hl-item:hover { background:#fafafa; }', + '.cc-hl-item .cc-dot { flex:0 0 26px; width:26px; height:26px; border-radius:50%; color:#fff; font:bold 12px/26px -apple-system,system-ui,sans-serif; text-align:center; }', + '.cc-hl-item .cc-text { flex:1; min-width:0; }', + '.cc-hl-item .cc-hh { font-weight:600; font-size:13px; color:#222; }', + '.cc-hl-item .cc-dd { font-size:12px; color:#555; margin-top:4px; line-height:1.5; white-space:pre-wrap; word-break:break-word; }' + ].join('\n'); + document.head.appendChild(style); + + const panel = document.createElement('div'); + panel.id = 'cc-hl-panel'; + panel.className = 'cc-hl-panel'; + panel.innerHTML = '<div class="cc-hl-panel-head">' + + '<span class="cc-title"></span>' + + '<button data-fold title="접기/펴기">▾</button>' + + '<button data-close title="닫기">×</button>' + + '</div>' + + '<div class="cc-hl-body"></div>'; + panel.querySelector('.cc-title').textContent = TITLE || '가이드'; + document.body.appendChild(panel); + const body = panel.querySelector('.cc-hl-body'); + + const foldBtn = panel.querySelector('[data-fold]'); + foldBtn.addEventListener('click', () => { + const col = panel.classList.toggle('collapsed'); + foldBtn.textContent = col ? '▸' : '▾'; + }); + + const pairs = []; + SPECS.forEach((spec, i) => { + const el = document.querySelector('[data-cc-step="' + (i + 1) + '"]'); + if (!el) return; + const color = colorFor(i); + const label = labelFor(i); + el.style.outline = '3px solid ' + color; + el.style.outlineOffset = '2px'; + el.setAttribute('data-cc-hl', '1'); + + const badge = document.createElement('div'); + badge.className = 'cc-hl-badge'; + badge.style.background = color; + badge.textContent = label; + document.body.appendChild(badge); + + const item = document.createElement('div'); + item.className = 'cc-hl-item'; + const desc = spec.desc ? String(spec.desc).replace(/\\n/g, '\n') : ''; + item.innerHTML = '<div class="cc-dot"></div><div class="cc-text"><div class="cc-hh"></div>' + (desc ? '<div class="cc-dd"></div>' : '') + '</div>'; + const dot = item.querySelector('.cc-dot'); + dot.style.background = color; + dot.textContent = label; + item.querySelector('.cc-hh').textContent = spec.heading || ''; + if (desc) item.querySelector('.cc-dd').textContent = desc; + item.addEventListener('click', () => { + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + const orig = el.style.boxShadow; + let c = 0; + const iv = setInterval(() => { + el.style.boxShadow = c % 2 === 0 ? ('0 0 28px 8px ' + color) : orig; + c++; + if (c > 5) { clearInterval(iv); el.style.boxShadow = orig; } + }, 150); + }); + body.appendChild(item); + + pairs.push({ el, badge, color }); + }); + + const update = () => { + pairs.forEach(({ el, badge }) => { + const r = el.getBoundingClientRect(); + if (r.width === 0 && r.height === 0) { badge.style.display = 'none'; return; } + badge.style.display = ''; + badge.style.top = Math.max(2, r.top - 14) + 'px'; + badge.style.left = Math.max(2, r.left - 14) + 'px'; + }); + }; + let raf = null; + const schedule = () => { + if (raf) return; + raf = requestAnimationFrame(() => { raf = null; update(); }); + }; + update(); + window.addEventListener('scroll', schedule, true); + window.addEventListener('resize', schedule); + const mo = new MutationObserver(schedule); + mo.observe(document.body, { subtree: true, attributes: true, childList: true }); + + const cleanup = () => { + window.removeEventListener('scroll', schedule, true); + window.removeEventListener('resize', schedule); + mo.disconnect(); + pairs.forEach(({ el, badge }) => { + el.style.outline = ''; + el.style.outlineOffset = ''; + el.style.boxShadow = ''; + el.removeAttribute('data-cc-hl'); + el.removeAttribute('data-cc-step'); + badge.remove(); + }); + panel.remove(); + const s = document.getElementById('cc-hl-style'); if (s) s.remove(); + window.__ccHl = undefined; + }; + panel.querySelector('[data-close]').addEventListener('click', cleanup); + window.__ccHl = { cleanup, update }; + + if (pairs.length > 0) pairs[0].el.scrollIntoView({ behavior: 'smooth', block: 'center' }); +} diff --git a/split-work-plugin/.claude-plugin/plugin.json b/split-work-plugin/.claude-plugin/plugin.json new file mode 100644 index 0000000..6729098 --- /dev/null +++ b/split-work-plugin/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "split-work-plugin", + "description": "Split current project work into parallel-safe task groups with worktree branch names and structured starting prompts", + "version": "1.0.0", + "author": { + "name": "Stefan Cho" + } +} diff --git a/split-work-plugin/README.md b/split-work-plugin/README.md new file mode 100644 index 0000000..092854f --- /dev/null +++ b/split-work-plugin/README.md @@ -0,0 +1,31 @@ +# split-work-plugin + +현재 프로젝트의 작업을 충돌 없는 병렬 묶음으로 쪼개고, 각 묶음을 별도 worktree에서 시작할 수 있도록 CLI 명령 + 시작 프롬프트를 생성한다. 시니어가 주니어들에게 업무를 분담하듯 의존성·파일 충돌을 분석해 안전한 lane을 제안한다. + +## 사용법 + +``` +/split-work +``` + +또는 자연어로 "작업 쪼개줘", "병렬로 진행할 수 있게 분리해줘". + +## 동작 + +1. `tasks/`, `specs/`, `git worktree list`, 최근 커밋 기반으로 현황 수집 +2. `depends_on` 충족 + 파일/패턴 충돌 여부로 그룹화 +3. 브랜치명 추천 (`worktree-task-{번호}` 패턴) +4. 각 task에 시작 프롬프트(XML) 생성 +5. `~/.claude/split-work/{project-slug}/{YYYY-MM-DD-HHmm}.md` 에 저장 + +## Pair + +- `writing-tasks-plugin` — task 분해 (split-work의 입력) +- `agent-team-plugin` — split-work 결과를 받아 worktree+팀 자동 구성 + +## Install + +```bash +/plugin marketplace add devstefancho/claude-plugins +/plugin install split-work-plugin@devstefancho-claude-plugins +``` diff --git a/split-work-plugin/skills/split-work/SKILL.md b/split-work-plugin/skills/split-work/SKILL.md new file mode 100644 index 0000000..f9b156a --- /dev/null +++ b/split-work-plugin/skills/split-work/SKILL.md @@ -0,0 +1,54 @@ +--- +name: split-work +description: "Split current project work into parallel-safe task groups with worktree branch names and structured starting prompts. Manual trigger via /split-work." +model: opus +context: fork +agent: work-status +allowed-tools: Read, Glob, Grep, Bash +--- + +# Split Work + +시니어 개발자가 주니어들에게 업무를 분담하듯, 현재 프로젝트의 작업을 충돌 없는 병렬 묶음으로 쪼개고 각 묶음을 별도 worktree 에서 실행할 수 있도록 CLI 명령 + 시작 프롬프트를 생성한다. + +## 실행 절차 + +1. **현황 수집** — `tasks/`, `specs/`, `DEPENDENCIES.md`, `git worktree list`, 최근 커밋을 읽는다. `scripts/task-status.ts` 같은 상태 스크립트가 있으면 먼저 실행. +2. **병렬성 판단** — 각 후보의 `depends_on` 충족 + 파일/패턴 충돌 여부를 확인해 그룹화한다. 같은 그룹 안의 task 는 동시에 worktree 를 열어도 안전해야 한다. +3. **브랜치명 추천** — `git log` 로 기존 컨벤션 확인 후 `worktree-task-{번호}` 또는 `worktree-task-{범위}x` 로 제안. +4. **프롬프트 생성** — `templates/prompt.xml` 을 읽어 각 task 에 채운다. +5. **파일 저장** — `templates/output.md` 포맷대로 채운 전문을 아래 경로에 저장한다: + ``` + ~/.claude/split-work/{project-slug}/{YYYY-MM-DD-HHmm}.md + ``` + - `{project-slug}` 결정 (worktree 안에서도 main repo 기준): + ```bash + main_repo="$(dirname "$(git rev-parse --git-common-dir)")" + slug="$(basename "$(dirname "$main_repo")")-$(basename "$main_repo")" + # 예: ~/works/runner/web → runner-web + ``` + basename 만 쓰면 `web` 같은 generic 이름이 다른 repo 와 충돌하므로 부모 한 단계 결합. + - 디렉토리 없으면 `mkdir -p` 로 생성. 타임스탬프는 KST 기준. +6. **출력** — 저장한 파일의 **전체 내용을 그대로** 반환한다. 맨 앞에 `💾 저장됨: <절대경로>` 한 줄 추가. + +## 출력 규칙 (호출자 assistant 에게) + +이 스킬 결과는 **fork 컨텍스트** 에서 반환되어 사용자에겐 직접 보이지 않는다. 호출자 assistant 는: + +- `<task>...</task>` XML 블록을 **축약·요약·생략 금지**. 사용자가 다음 worktree 의 첫 메시지로 그대로 복사하는 원본이다. +- "위 결과에 포함됨" / "XML 로 첨부됨" 같은 참조 문구로 대체 금지. +- 만약 응답 길이상 부득이 축약한다면, **반드시 `💾 저장됨` 경로를 사용자에게 노출** 해 파일을 직접 열 수 있게 한다. + +## 자유도 원칙 + +구현 에이전트의 판단 영역을 침범하지 않는다. + +- **금지:** 파일 경로·함수명·라이브러리·구현 순서 지시 +- **허용:** 사용자 가치·성공 기준·절대 제약(API 계약·보안·순수성·롤백성)·스펙 참조 위치 + +"어떻게" 는 에이전트가 스펙을 읽고 스스로 결정한다. + +## 인수 + +- 없음 → 프로젝트 전체 대상 +- `phase-N` 또는 task 번호 목록 → 해당 범위만 diff --git a/split-work-plugin/skills/split-work/templates/output.md b/split-work-plugin/skills/split-work/templates/output.md new file mode 100644 index 0000000..2788b04 --- /dev/null +++ b/split-work-plugin/skills/split-work/templates/output.md @@ -0,0 +1,50 @@ +<!-- SPLIT-WORK-OUTPUT: 호출자는 아래 전문(특히 <task> XML)을 축약·생략 금지. 사용자가 worktree 첫 메시지로 복사하는 원본임. --> + +💾 저장됨: {파일 절대경로} + +# 작업 현황 요약 + +- 진행률: {완료/전체} +- 다음 우선순위: {top 3-5} +- 병렬 가능 묶음: {A, B...} + +--- + +# 🟢 즉시 병렬 가능 (묶음 A) + +## Task {번호} — {제목} +```bash +cld -w "worktree-task-{번호}" +``` +**시작 프롬프트:** +```xml +<task id="...">...</task> +``` + +(묶음 안의 나머지 task 반복) + +--- + +# 🟡 순차 진행 (묶음 A 완료 후) + +... + +--- + +# 🟣 독립 진행 + +... + +--- + +# 한번에 붙여넣기 (묶음 A) + +```bash +cld -w "worktree-task-045" +cld -w "worktree-task-042" +cld -w "worktree-task-097" +``` + +--- + +**범례:** 🟢 즉시 병렬 / 🟡 선행 대기 / 🟣 독립 진행 diff --git a/split-work-plugin/skills/split-work/templates/prompt.xml b/split-work-plugin/skills/split-work/templates/prompt.xml new file mode 100644 index 0000000..d9905c2 --- /dev/null +++ b/split-work-plugin/skills/split-work/templates/prompt.xml @@ -0,0 +1,34 @@ +<task id="{번호}"> + <functional> + <goal> + 한 문단으로 "이 작업이 사용자/조직에게 주는 가치" + </goal> + <user-scenarios> + <scenario>구체적 사용 상황 1</scenario> + <scenario>구체적 사용 상황 2</scenario> + </user-scenarios> + <success-criteria> + <item>사용자 관점의 성공 기준</item> + </success-criteria> + <out-of-scope>이번 작업에 포함되지 않는 영역 (선택)</out-of-scope> + </functional> + + <technical> + <scope>작업 범위를 한 줄로 (layer/역할 정도만)</scope> + <dependencies>선행 task 나 선결 조건 (선택)</dependencies> + <constraints> + <item>절대 제약 — API 계약, 순수성, 보안, 롤백성 등</item> + </constraints> + <references> + <ref>tasks/task-{번호}.md</ref> + <ref>specs/ ({번호} 관련)</ref> + </references> + <done> + <item>완료 판정 기준 (테스트 범위·검증 방법) — 수단은 에이전트 위임</item> + </done> + </technical> + + <kickoff> + 스펙과 기존 코드 구조를 먼저 파악한 뒤, 짧게 접근 방안을 제시하고 시작한다. + </kickoff> +</task>