Frontend Bug Verification Skill
原則:GitHub Issue 留言區 = 唯一真相來源。 所有除錯過程(重現、根因、修復、驗證)都必須完整記錄在 Issue 留言區。 對話會消失,Issue 留言不會。任何人隨時都能回顧完整脈絡。
Philosophy: "SCREENSHOT BUGS. Every bug gets a screenshot." - AI QA Engineer 2026
Usage
/frontend-bug-verification {N}
Where {N} is the GitHub issue number.
IMMUTABLE RULES (NEVER BYPASS)
Rule 1: NO MERGE / NO PUSH to protected branches
FORBIDDEN commands (no exceptions):
gh pr merge
git push origin staging
git push origin main
gh issue close
ALLOWED commands:
git push origin fix/issue-{N}-* (feature branch only)
gh pr create (create, not merge)
gh issue comment (comment, not close)
Merging and deploying = USER's decision. Period.
Rule 4: GATE CHECK ENFORCER (PROGRAMMATIC — NOT OPTIONAL)
Every step MUST call gate-check.sh BEFORE and AFTER:
BEFORE starting step N:
./gate-check.sh {ISSUE_NUM} {N} check
→ Exits with error if ANY previous step is not "passed"
→ Agent CANNOT proceed if this fails
AFTER gate check passes:
./gate-check.sh {ISSUE_NUM} {N} pass
→ Marks step as "passed" in state file
→ Only then can step N+1 begin
State file: /tmp/bugfix/issue-{N}/gate-state.json
Script: .claude/skills/frontend-bug-verification/gate-check.sh
If agent skips calling gate-check.sh → state file won't have "passed"
→ next step's "check" will FAIL → agent is BLOCKED.
This is CODE enforcement, not honor-system enforcement.
Rule 5: USER MUST VERIFY ON PREVIEW URL BEFORE MERGE
Workflow AFTER this skill completes: 1. Agent posts BEFORE/AFTER to issue (Step 10) ← agent does this 2. Agent creates PR (Step 12) ← agent does this 3. Agent provides preview URL to user ← agent does this 4. USER opens preview URL and tests manually ← USER does this 5. USER comments "通過" / "OK" / "LGTM" ← USER does this 6. ONLY THEN can PR be merged ← USER decides Agent may NOT: - Merge PR even if all tests pass - Claim "verified" without user confirmation - Skip providing the preview URL - Close the issue No user confirmation on preview URL = No merge. Period.
Rule 2: NO STEP SKIPPING
Every step has a GATE CHECK. Gate check FAIL → HARD STOP → fix → retry. You may NOT proceed to Step N+1 until Step N passes.
Rule 3: SCREENSHOTS MUST BE FILES ON DISK
Screenshots must be saved as files in .claude/evidence/issue-{N}/.
Acceptable sources:
- Playwright: page.screenshot({ path: '...' })
- Playwright CLI: npx playwright screenshot "url" "path"
- MCP Chrome → Playwright save (MCP Chrome 看 + Playwright 存檔)
Evidence = file verified by: ls -lh <file> && test -s <file>
Note: This is Phase 2 (Worktree + Playwright).
For quick verification, use /quick-verify first.
Only use this skill when:
- Need BEFORE/AFTER side-by-side comparison
- Fix not yet deployed to staging (need localhost worktree)
- Need dual-browser sync testing
- User explicitly requests worktree mode
Architecture: Worktree-Based Dual Environment
Main repo (~/project/career-creator-card/)
└── Current branch (whatever user is on, untouched)
Worktrees:
/tmp/worktrees/issue-{N}/
├── buggy/ ← detached HEAD at pre-fix commit (has the bug)
│ ├── frontend/ → port 3001
│ └── backend/ → port 8001
└── fix/ ← fix/issue-{N}-* branch (has the fix)
├── frontend/ → port 3000
└── backend/ → port 8000
Why worktrees + commit-based buggy?
- •Both environments run simultaneously (no branch switching)
- •No remote staging manipulation (no revert/re-merge needed)
- •Buggy worktree uses detached HEAD → no branch conflict even if staging has the fix
- •Backend restarts automatically (separate processes)
- •Reproducible: anyone can re-run with the same commits
Workflow (12 Steps with Gate Checks)
Step 0: Setup
ISSUE_NUM={N}
EVIDENCE_DIR="/tmp/bugfix/issue-${ISSUE_NUM}"
WORKTREE_DIR="/tmp/worktrees/issue-${ISSUE_NUM}"
# Clean previous runs
rm -rf "$EVIDENCE_DIR" "$WORKTREE_DIR"
mkdir -p "$EVIDENCE_DIR"
mkdir -p "$WORKTREE_DIR"
# Save config for later steps
cat > "$EVIDENCE_DIR/config.env" << EOF
ISSUE_NUM=${ISSUE_NUM}
EVIDENCE_DIR=${EVIDENCE_DIR}
WORKTREE_DIR=${WORKTREE_DIR}
BUGGY_DIR=${WORKTREE_DIR}/buggy
FIX_DIR=${WORKTREE_DIR}/fix
BEFORE_PORT_FE=3001
BEFORE_PORT_BE=8001
AFTER_PORT_FE=3000
AFTER_PORT_BE=8000
EOF
GATE CHECK 0:
# Run the actual gate check
test -d "$EVIDENCE_DIR" && test -f "$EVIDENCE_DIR/config.env" && echo "GATE 0: PASS" || { echo "GATE 0: FAIL"; exit 1; }
# ✅ Mark step 0 as passed in state file (MANDATORY)
GATE="$REPO_ROOT/.claude/skills/frontend-bug-verification/gate-check.sh"
"$GATE" "$ISSUE_NUM" 0 pass
EVERY STEP follows this pattern:
- •
$GATE $ISSUE_NUM {N} check— before starting (blocks if previous steps incomplete)- •Do the work
- •Run the gate check logic
- •
$GATE $ISSUE_NUM {N} pass— after gate check passes (unlocks next step)
Step 1: Read Issue Details
gh issue view $ISSUE_NUM --json title,body,labels > "$EVIDENCE_DIR/issue.json" # Extract and display: # - Bug description # - Reproduction steps (EXACT click-by-click) # - Expected vs actual behavior
GATE CHECK 1:
test -s "$EVIDENCE_DIR/issue.json" && echo "GATE 1: PASS" || echo "GATE 1: FAIL"
Step 2: Create Worktrees
REPO_ROOT=$(git rev-parse --show-toplevel)
cd "$REPO_ROOT"
git fetch origin 2>/dev/null
# Identify the fix branch
FIX_BRANCH=$(git branch -a | grep "fix/issue-${ISSUE_NUM}" | head -1 | xargs)
FIX_BRANCH=${FIX_BRANCH#remotes/origin/}
echo "Fix branch: $FIX_BRANCH"
# Find the BUGGY commit: the parent of the fix branch's merge-base with staging
# This gives us the state of staging BEFORE the fix was applied
FIX_FIRST_COMMIT=$(git log staging.."$FIX_BRANCH" --oneline --reverse | head -1 | awk '{print $1}')
BUGGY_COMMIT=$(git merge-base staging "$FIX_BRANCH")
# If fix was already merged to staging, use the commit BEFORE the merge
MERGED_PR=$(gh pr list --state merged --base staging --search "head:fix/issue-${ISSUE_NUM}" --json mergeCommit -q '.[0].mergeCommit.oid' 2>/dev/null)
if [ -n "$MERGED_PR" ]; then
BUGGY_COMMIT=$(git rev-parse "${MERGED_PR}^")
fi
echo "Buggy commit: $BUGGY_COMMIT"
# Create worktrees
# Buggy: detached HEAD at the pre-fix commit (no branch conflict)
git worktree add --detach "$WORKTREE_DIR/buggy" "$BUGGY_COMMIT" 2>&1
# Fix: checkout the fix branch
git worktree add "$WORKTREE_DIR/fix" "$FIX_BRANCH" 2>&1
# Verify
echo "Buggy worktree ($(git -C "$WORKTREE_DIR/buggy" rev-parse --short HEAD)):"
git -C "$WORKTREE_DIR/buggy" log --oneline -1
echo "Fix worktree:"
git -C "$WORKTREE_DIR/fix" branch --show-current
GATE CHECK 2:
BUGGY_HEAD=$(git -C "$WORKTREE_DIR/buggy" rev-parse --short HEAD 2>/dev/null) FIX_BRANCH=$(git -C "$WORKTREE_DIR/fix" branch --show-current 2>/dev/null) if [ -n "$BUGGY_HEAD" ] && [ -n "$FIX_BRANCH" ]; then echo "GATE 2: PASS - buggy=$BUGGY_HEAD, fix=$FIX_BRANCH" else echo "GATE 2: FAIL - worktree creation failed" [ -z "$BUGGY_HEAD" ] && echo " buggy worktree missing" [ -z "$FIX_BRANCH" ] && echo " fix worktree missing" echo " Try: git worktree list && git worktree prune" fi
Step 3: Install Dependencies + Configure .env in Both Worktrees
CRITICAL: Frontend .env.local MUST set NEXT_PUBLIC_API_URL to the correct backend port.
Without this, Next.js defaults to http://localhost:8000 — the BUGGY frontend would hit the FIX backend!
# --- Copy .env files FIRST (before npm install) --- # Backend .env (DB connection, secrets) MAIN_REPO=$(git rev-parse --show-toplevel) cp "$MAIN_REPO/backend/.env" "$WORKTREE_DIR/buggy/backend/.env" 2>/dev/null cp "$MAIN_REPO/backend/.env" "$WORKTREE_DIR/fix/backend/.env" 2>/dev/null # Frontend .env.local — MUST set correct API_URL per worktree! # Copy from main repo as base, then override API_URL cp "$MAIN_REPO/frontend/.env.local" "$WORKTREE_DIR/buggy/frontend/.env.local" 2>/dev/null cp "$MAIN_REPO/frontend/.env.local" "$WORKTREE_DIR/fix/frontend/.env.local" 2>/dev/null # CRITICAL: Override API_URL for each worktree # Buggy frontend → buggy backend (port 8001) sed -i '' 's|NEXT_PUBLIC_API_URL=.*|NEXT_PUBLIC_API_URL=http://localhost:8001|' "$WORKTREE_DIR/buggy/frontend/.env.local" sed -i '' 's|NEXT_PUBLIC_APP_URL=.*|NEXT_PUBLIC_APP_URL=http://localhost:3001|' "$WORKTREE_DIR/buggy/frontend/.env.local" # Fix frontend → fix backend (port 8000) sed -i '' 's|NEXT_PUBLIC_API_URL=.*|NEXT_PUBLIC_API_URL=http://localhost:8000|' "$WORKTREE_DIR/fix/frontend/.env.local" sed -i '' 's|NEXT_PUBLIC_APP_URL=.*|NEXT_PUBLIC_APP_URL=http://localhost:3000|' "$WORKTREE_DIR/fix/frontend/.env.local" # Verify API_URL is set correctly echo "Buggy API_URL: $(grep NEXT_PUBLIC_API_URL "$WORKTREE_DIR/buggy/frontend/.env.local")" echo "Fix API_URL: $(grep NEXT_PUBLIC_API_URL "$WORKTREE_DIR/fix/frontend/.env.local")" # --- Install deps --- # Install frontend deps cd "$WORKTREE_DIR/buggy/frontend" && npm install --legacy-peer-deps 2>&1 | tail -3 cd "$WORKTREE_DIR/fix/frontend" && npm install --legacy-peer-deps 2>&1 | tail -3 # Install backend deps (if using venv) cd "$WORKTREE_DIR/buggy/backend" && pip install -r requirements.txt 2>&1 | tail -3 cd "$WORKTREE_DIR/fix/backend" && pip install -r requirements.txt 2>&1 | tail -3 # Install Playwright browsers cd "$WORKTREE_DIR/buggy/frontend" && npx playwright install chromium 2>/dev/null
GATE CHECK 3:
BUGGY_MODULES="$WORKTREE_DIR/buggy/frontend/node_modules"
FIX_MODULES="$WORKTREE_DIR/fix/frontend/node_modules"
BUGGY_ENV="$WORKTREE_DIR/buggy/frontend/.env.local"
FIX_ENV="$WORKTREE_DIR/fix/frontend/.env.local"
PASS=true
[ -d "$BUGGY_MODULES" ] || { echo "FAIL: Missing $BUGGY_MODULES"; PASS=false; }
[ -d "$FIX_MODULES" ] || { echo "FAIL: Missing $FIX_MODULES"; PASS=false; }
[ -f "$BUGGY_ENV" ] || { echo "FAIL: Missing $BUGGY_ENV"; PASS=false; }
[ -f "$FIX_ENV" ] || { echo "FAIL: Missing $FIX_ENV"; PASS=false; }
grep -q "localhost:8001" "$BUGGY_ENV" 2>/dev/null || { echo "FAIL: Buggy .env.local not pointing to port 8001"; PASS=false; }
grep -q "localhost:8000" "$FIX_ENV" 2>/dev/null || { echo "FAIL: Fix .env.local not pointing to port 8000"; PASS=false; }
if [ "$PASS" = true ]; then
echo "GATE 3: PASS - deps installed, .env.local configured with correct ports"
else
echo "GATE 3: FAIL"
fi
Step 4: Start Both Environments
Start backends and frontends in background. Use different ports.
# --- BUGGY environment (staging branch) --- # Backend on port 8001 cd "$WORKTREE_DIR/buggy/backend" PORT=8001 uvicorn app.main:app --port 8001 --host 0.0.0.0 & BUGGY_BE_PID=$! echo "Buggy backend PID: $BUGGY_BE_PID (port 8001)" # Frontend on port 3001 (with API pointing to buggy backend) cd "$WORKTREE_DIR/buggy/frontend" NEXT_PUBLIC_API_URL=http://localhost:8001 PORT=3001 npm run dev & BUGGY_FE_PID=$! echo "Buggy frontend PID: $BUGGY_FE_PID (port 3001)" # --- FIX environment (fix branch) --- # Backend on port 8000 cd "$WORKTREE_DIR/fix/backend" PORT=8000 uvicorn app.main:app --port 8000 --host 0.0.0.0 & FIX_BE_PID=$! echo "Fix backend PID: $FIX_BE_PID (port 8000)" # Frontend on port 3000 (with API pointing to fix backend) cd "$WORKTREE_DIR/fix/frontend" NEXT_PUBLIC_API_URL=http://localhost:8000 PORT=3000 npm run dev & FIX_FE_PID=$! echo "Fix frontend PID: $FIX_FE_PID (port 3000)" # Save PIDs for cleanup cat > "$EVIDENCE_DIR/pids.env" << EOF BUGGY_BE_PID=$BUGGY_BE_PID BUGGY_FE_PID=$BUGGY_FE_PID FIX_BE_PID=$FIX_BE_PID FIX_FE_PID=$FIX_FE_PID EOF # Wait for servers to start sleep 10
GATE CHECK 4:
BUGGY_OK=$(curl -sf -o /dev/null http://localhost:8001/health 2>/dev/null && echo "yes" || echo "no") FIX_OK=$(curl -sf -o /dev/null http://localhost:8000/health 2>/dev/null && echo "yes" || echo "no") BUGGY_FE_OK=$(curl -sf -o /dev/null http://localhost:3001 2>/dev/null && echo "yes" || echo "no") FIX_FE_OK=$(curl -sf -o /dev/null http://localhost:3000 2>/dev/null && echo "yes" || echo "no") if [ "$BUGGY_OK" = "yes" ] && [ "$FIX_OK" = "yes" ]; then echo "GATE 4: PASS" echo " Buggy: BE=$BUGGY_OK FE=$BUGGY_FE_OK (ports 8001/3001)" echo " Fix: BE=$FIX_OK FE=$FIX_FE_OK (ports 8000/3000)" else echo "GATE 4: FAIL - servers not responding" echo " Buggy backend (8001): $BUGGY_OK" echo " Fix backend (8000): $FIX_OK" echo " Check logs and retry" fi
NOTE: If backends need environment variables (.env files), copy them into worktrees first.
Step 5: Generate BEFORE Test
CRITICAL PRE-STEP: Before writing the test, READ the actual page source to determine correct selectors.
Do NOT assume standard HTML attributes (e.g., input[name="X"]) — inspect the actual code.
# Read the page source to find correct selectors
cat "$WORKTREE_DIR/buggy/frontend/src/app/{relevant-page}/page.tsx" | grep -E 'id=|name=|type=|placeholder=|data-testid=' | head -20
Common pitfalls:
- •React forms often use
id="name"notname="name"→ use#nameselector - •Select dropdowns may have no options in empty DB → test must handle "new" flow
- •Some forms use controlled components with no HTML
nameattribute at all
Create frontend/e2e/issue-{N}-before.spec.ts directly in the BUGGY worktree:
import { test, expect } from '@playwright/test';
// BEFORE test targets the BUGGY environment (staging branch, local)
const BUGGY_URL = 'http://localhost:3001';
const TEST_EMAIL = 'demo.counselor@example.com';
const TEST_PASSWORD = 'demo123';
test.describe('Issue #{N} BEFORE - {title}', () => {
test.setTimeout(60000);
test.beforeEach(async ({ page }) => {
await page.goto(`${BUGGY_URL}/login`);
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard', { timeout: 15000 });
});
test('BEFORE: {description} - bug exists', async ({ page }) => {
// TODO: Replace with actual reproduction steps from issue
// Use selectors from the pre-step inspection above
// Capture screenshot BEFORE fix
await page.screenshot({
path: '/tmp/bugfix/issue-{N}/01-before-error.png',
fullPage: true,
});
// Assert bug exists
});
});
Write spec directly to buggy worktree (NOT main repo):
# Write directly to worktree — do NOT copy from main repo
# File: $WORKTREE_DIR/buggy/frontend/e2e/issue-{N}-before.spec.ts
GATE CHECK 5:
test -f "$WORKTREE_DIR/buggy/frontend/e2e/issue-${ISSUE_NUM}-before.spec.ts" && echo "GATE 5: PASS" || echo "GATE 5: FAIL"
Step 6: Execute BEFORE Test
cd "$WORKTREE_DIR/buggy/frontend"
npx playwright test "e2e/issue-${ISSUE_NUM}-before.spec.ts" --headed 2>&1 \
| tee "$EVIDENCE_DIR/before-test-output.txt" \
| tail -20
GATE CHECK 6 (CRITICAL):
BEFORE_FILE=$(ls -1 "$EVIDENCE_DIR"/01-before-*.png 2>/dev/null | head -1)
if [ -n "$BEFORE_FILE" ] && [ -s "$BEFORE_FILE" ]; then
SIZE=$(ls -lh "$BEFORE_FILE" | awk '{print $5}')
echo "GATE 6: PASS - BEFORE screenshot ($SIZE): $BEFORE_FILE"
else
echo "GATE 6: FAIL - NO BEFORE SCREENSHOT"
echo "HARD STOP: Cannot proceed without BEFORE evidence"
echo "Debug: cat $EVIDENCE_DIR/before-test-output.txt"
fi
HARD STOP if FAIL → debug test → retry → do NOT proceed.
Step 7: Generate AFTER Test
Create frontend/e2e/issue-{N}-after.spec.ts in the FIX worktree:
import { test, expect } from '@playwright/test';
// AFTER test targets the FIX environment (fix branch, local)
const FIX_URL = 'http://localhost:3000';
const TEST_EMAIL = 'demo.counselor@example.com';
const TEST_PASSWORD = 'demo123';
test.describe('Issue #{N} AFTER - {title}', () => {
test.setTimeout(60000);
test.beforeEach(async ({ page }) => {
await page.goto(`${FIX_URL}/login`);
await page.fill('input[type="email"]', TEST_EMAIL);
await page.fill('input[type="password"]', TEST_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard', { timeout: 15000 });
});
test('AFTER: {description} - bug fixed', async ({ page }) => {
// SAME reproduction steps as BEFORE test
// Capture screenshot AFTER fix
await page.screenshot({
path: '/tmp/bugfix/issue-{N}/05-after-success.png',
fullPage: true,
});
// Assert bug is GONE
});
});
Copy to FIX worktree:
cp frontend/e2e/issue-{N}-after.spec.ts "$WORKTREE_DIR/fix/frontend/e2e/"
GATE CHECK 7:
test -f "$WORKTREE_DIR/fix/frontend/e2e/issue-${ISSUE_NUM}-after.spec.ts" && echo "GATE 7: PASS" || echo "GATE 7: FAIL"
Step 8: Execute AFTER Test
cd "$WORKTREE_DIR/fix/frontend"
npx playwright test "e2e/issue-${ISSUE_NUM}-after.spec.ts" --headed 2>&1 \
| tee "$EVIDENCE_DIR/after-test-output.txt" \
| tail -20
GATE CHECK 8 (CRITICAL):
AFTER_FILE=$(ls -1 "$EVIDENCE_DIR"/05-after-*.png 2>/dev/null | head -1)
if [ -n "$AFTER_FILE" ] && [ -s "$AFTER_FILE" ]; then
SIZE=$(ls -lh "$AFTER_FILE" | awk '{print $5}')
echo "GATE 8: PASS - AFTER screenshot ($SIZE): $AFTER_FILE"
else
echo "GATE 8: FAIL - NO AFTER SCREENSHOT"
echo "HARD STOP: Fix not verified. Debug and retry."
echo "Debug: cat $EVIDENCE_DIR/after-test-output.txt"
fi
HARD STOP if FAIL → fix not working → debug → retry.
Step 9: Verify Both Screenshots
echo "=== FINAL EVIDENCE CHECK ==="
BEFORE=$(ls -1 "$EVIDENCE_DIR"/01-before-*.png 2>/dev/null | head -1)
AFTER=$(ls -1 "$EVIDENCE_DIR"/05-after-*.png 2>/dev/null | head -1)
PASS=true
if [ -n "$BEFORE" ] && [ -s "$BEFORE" ]; then
echo "BEFORE: $(ls -lh "$BEFORE" | awk '{print $5, $9}')"
else
echo "BEFORE: MISSING"; PASS=false
fi
if [ -n "$AFTER" ] && [ -s "$AFTER" ]; then
echo "AFTER: $(ls -lh "$AFTER" | awk '{print $5, $9}')"
else
echo "AFTER: MISSING"; PASS=false
fi
if [ "$PASS" = true ]; then
echo "=== GATE 9: PASS ==="
else
echo "=== GATE 9: FAIL - INCOMPLETE EVIDENCE ==="
echo "HARD STOP: Go back to the missing step."
fi
Step 10: Upload Evidence + Post to GitHub Issue
This step is NOT optional. Rule 2 applies: NO STEP SKIPPING.
# --- 10a: Copy screenshots to repo evidence folder ---
REPO_ROOT=$(git rev-parse --show-toplevel)
EVIDENCE_REPO_DIR="$REPO_ROOT/.claude/evidence/issue-${ISSUE_NUM}"
mkdir -p "$EVIDENCE_REPO_DIR"
# Copy BEFORE and AFTER screenshots
cp "$EVIDENCE_DIR"/01-before-*.png "$EVIDENCE_REPO_DIR/"
cp "$EVIDENCE_DIR"/02-after-*.png "$EVIDENCE_REPO_DIR/" 2>/dev/null
cp "$EVIDENCE_DIR"/05-after-*.png "$EVIDENCE_REPO_DIR/" 2>/dev/null
ls -lh "$EVIDENCE_REPO_DIR/"
# --- 10b: Commit and push evidence to staging ---
cd "$REPO_ROOT"
git add ".claude/evidence/issue-${ISSUE_NUM}/"
git commit -m "docs: add issue #${ISSUE_NUM} BEFORE/AFTER verification screenshots"
git push origin staging
# --- 10c: Post comment with GitHub raw URLs ---
# IMPORTANT: Use raw.githubusercontent.com URLs so images render on GitHub
REPO_URL="https://raw.githubusercontent.com/{owner}/{repo}/staging"
# Get actual repo info from git remote
REMOTE_URL=$(git remote get-url origin)
OWNER_REPO=$(echo "$REMOTE_URL" | sed 's|.*github.com[:/]||' | sed 's|\.git$||')
REPO_URL="https://raw.githubusercontent.com/${OWNER_REPO}/staging"
BEFORE_IMG=$(ls -1 "$EVIDENCE_REPO_DIR"/01-before-*.png 2>/dev/null | head -1 | xargs basename)
AFTER_IMG=$(ls -1 "$EVIDENCE_REPO_DIR"/02-after-*.png "$EVIDENCE_REPO_DIR"/05-after-*.png 2>/dev/null | head -1 | xargs basename)
gh issue comment $ISSUE_NUM --body "$(cat <<EOF
## Worktree Verification — Issue #${ISSUE_NUM}
### 1. 重現過程
- **環境**:Worktree — buggy (localhost:3001/8001) vs fix (localhost:3000/8000)
- **buggy commit**:$(git -C "$WORKTREE_DIR/buggy" rev-parse --short HEAD 2>/dev/null || echo "N/A")
- **fix branch**:$(git -C "$WORKTREE_DIR/fix" branch --show-current 2>/dev/null || echo "N/A")
- **操作步驟**:
1. 登入 demo.counselor@example.com
2. [具體重現步驟...]
### 2. BEFORE(Bug 存在)

- **觀察**:[描述截圖中看到的 bug]
### 3. 根因分析
- **為什麼出錯**:[簡述根因]
- **相關檔案**:\`path/to/file.ts:line\`
- **影響範圍**:[哪些功能受影響]
### 4. 修復內容
- **PR**:#XX
- **改了什麼**:[一句話描述修改]
- **Branch**:\`fix/issue-${ISSUE_NUM}-*\`
### 5. AFTER(Bug 修復)

- **觀察**:[描述截圖中 bug 已消失]
### 6. 結論
- [ ] Bug 確認修復,可關閉(待用戶確認)
- [ ] Bug 仍存在,需進一步調查
### What This Does NOT Do
- Does NOT merge to staging (user decision)
- Does NOT close the issue (user decision)
EOF
)"
GATE CHECK 10 (CRITICAL):
# Verify: (a) images committed, (b) comment posted with image URLs PASS=true # Check images exist in repo EVIDENCE_COUNT=$(ls -1 "$EVIDENCE_REPO_DIR"/*.png 2>/dev/null | wc -l | xargs) if [ "$EVIDENCE_COUNT" -ge 2 ]; then echo "Images in repo: $EVIDENCE_COUNT files" else echo "FAIL: Only $EVIDENCE_COUNT images in $EVIDENCE_REPO_DIR"; PASS=false fi # Check comment was posted (latest comment should contain "BEFORE" and "AFTER") LATEST_COMMENT=$(gh issue view $ISSUE_NUM --comments --json comments -q '.comments[-1].body' 2>/dev/null) if echo "$LATEST_COMMENT" | grep -q "BEFORE" && echo "$LATEST_COMMENT" | grep -q "AFTER"; then echo "Comment posted with BEFORE/AFTER sections" else echo "FAIL: Latest comment missing BEFORE/AFTER content"; PASS=false fi # Check comment contains image URLs (not local paths) if echo "$LATEST_COMMENT" | grep -q "raw.githubusercontent.com"; then echo "Comment contains GitHub image URLs" else echo "FAIL: Comment uses local paths instead of GitHub URLs"; PASS=false fi if [ "$PASS" = true ]; then echo "GATE 10: PASS - Evidence uploaded and posted to issue" else echo "GATE 10: FAIL - HARD STOP" echo "Fix the issue above and retry Step 10." fi
HARD STOP if FAIL → evidence not visible on GitHub → fix and retry.
Step 11: Cleanup Worktrees and Processes
# Kill background processes source "$EVIDENCE_DIR/pids.env" 2>/dev/null kill $BUGGY_BE_PID $BUGGY_FE_PID $FIX_BE_PID $FIX_FE_PID 2>/dev/null # Remove worktrees cd "$(git rev-parse --show-toplevel)" git worktree remove "$WORKTREE_DIR/buggy" --force 2>/dev/null git worktree remove "$WORKTREE_DIR/fix" --force 2>/dev/null rm -rf "$WORKTREE_DIR" echo "Cleanup complete. Evidence preserved at: $EVIDENCE_DIR/" ls -lh "$EVIDENCE_DIR/"
Step 12: Create PR + Provide Preview URL (do NOT merge)
# Push fix branch (feature branch only!)
FIX_BRANCH=$(git -C "$WORKTREE_DIR/fix" branch --show-current 2>/dev/null || echo "fix/issue-${ISSUE_NUM}-*")
git push origin "$FIX_BRANCH"
# Create PR
gh pr create \
--base staging \
--head "$FIX_BRANCH" \
--title "fix: {short description} (#${ISSUE_NUM})" \
--body "$(cat <<EOF
## Summary
Fixes #${ISSUE_NUM}
## Root Cause
[explanation]
## Evidence
See issue #${ISSUE_NUM} comment for BEFORE/AFTER screenshots.
## Test Plan
- [ ] BEFORE: \`npx playwright test e2e/issue-${ISSUE_NUM}-before.spec.ts\`
- [ ] AFTER: \`npx playwright test e2e/issue-${ISSUE_NUM}-after.spec.ts\`
- [ ] **User verified on preview URL**
EOF
)"
GATE CHECK 12: PR created successfully (URL returned).
Step 13: HAND OFF TO USER — Provide Preview URL and WAIT
This is the FINAL step the agent performs. After this, the agent STOPS.
Output to user:
====================================
Issue #{N} 修復完成,等待您驗證
====================================
PR: [PR URL]
Issue: https://github.com/{owner}/{repo}/issues/{N}
Preview URL (PR deploy):
Frontend: https://career-creator-frontend-staging-849078733818.asia-east1.run.app
(or PR preview URL if available)
請您:
1. 打開 Preview URL 手動測試
2. 確認 bug 已修復
3. 在 Issue 留言「通過」或「OK」
4. 然後告訴我可以 merge
⚠️ 在您確認之前,我不會 merge PR 或 close issue。
====================================
Agent STOPS HERE. Does NOT:
- •Merge the PR
- •Close the issue
- •Say "verified" or "done"
- •Proceed to any further action
Only when user explicitly says "merge" / "通過" / "OK" / "可以 merge":
- •THEN (and only then) may the agent run
gh pr merge
Hard Stop Conditions
| Condition | Action | May NOT Do |
|---|---|---|
| Worktree creation fails (Step 2) | STOP. Check branch exists. | Proceed without worktree |
| .env.local missing/wrong port (Step 3) | STOP. Create/fix .env.local. | Start servers without env |
| Servers not responding (Step 4) | STOP. Check ports, logs. | Run test against dead server |
| BEFORE screenshot missing (Step 6) | STOP. Debug test. Retry. | Proceed to Step 7 |
| AFTER screenshot missing (Step 8) | STOP. Fix broken. Debug. | Proceed to Step 9 |
| Either screenshot missing (Step 9) | STOP. Go back to missing step. | Post to issue |
| Evidence not on GitHub (Step 10) | STOP. Upload + repost. | Skip posting, claim "done" |
| PR not created (Step 12) | STOP. Fix and retry. | Skip PR creation |
| User has NOT verified preview URL (Step 13) | STOP. Provide URL. WAIT. | Merge PR, claim "done" |
| User has not said "merge" | STOP. | Merge PR |
| User has not said "close" | STOP. | Close issue |
Forbidden Actions
gh pr merge # FORBIDDEN git push origin staging # FORBIDDEN git push origin main # FORBIDDEN gh issue close # FORBIDDEN
Worktree Troubleshooting
"fatal: '{path}' is already checked out"
# Another worktree or the main repo has this branch checked out git worktree list # Remove stale worktrees git worktree prune
Port already in use
lsof -i :3000 | grep LISTEN kill <PID>
Backend needs .env
# Copy .env from main repo to worktrees cp .env "$WORKTREE_DIR/buggy/backend/.env" cp .env "$WORKTREE_DIR/fix/backend/.env"
Database connection
Both worktrees connect to the SAME database (staging). This is usually fine for read-heavy verification tests. For write tests, be aware of data conflicts.
Playwright Best Practices
// Effective selectors
await page.getByRole('button', { name: '刪除' }).click();
await page.click('[data-testid="delete-button"]');
// Wait strategies
const response = await page.waitForResponse(
res => res.url().includes('/api/') && res.status() === 200
);
await page.waitForSelector('.success-message', { state: 'visible' });
// Timeout for slow starts
test.setTimeout(60000);
Success Criteria
ALL must be true:
- • BEFORE screenshot at
$EVIDENCE_DIR/01-before-*.png(> 0 bytes) - • AFTER screenshot at
$EVIDENCE_DIR/02-after-*.pngor05-after-*.png(> 0 bytes) - • Bug visible in BEFORE, gone in AFTER
- • Evidence uploaded to
.claude/evidence/issue-{N}/and pushed - • Evidence posted to GitHub issue with GitHub image URLs
- • PR created (NOT merged)
- • Preview URL provided to user
- • User confirmed on preview URL (留言「通過」/「OK」/「LGTM」)
- • Issue left OPEN (NOT closed)
- • Worktrees cleaned up
No screenshot = No verification = Not done. No user preview verification = No merge. Period. No user approval = No merge = No deploy.
Lessons Learned from Benchmarks
Benchmark 2026-02-16: Issue #14 (Room creation Network Error)
Bugs found and fixed in this skill:
- •
.env.localwith correctNEXT_PUBLIC_API_URLis REQUIRED per worktree- •Without it, Next.js defaults to
http://localhost:8000— buggy frontend hits fix backend - •Step 3 now creates
.env.localwith port-specific API_URL for each worktree - •Gate check 3 now verifies the correct port is in each
.env.local
- •Without it, Next.js defaults to
- •
Next.js requires restart after
.env.localchanges- •If you create/modify
.env.localafternpm run dev, you must restart the frontend - •Step 4 should start frontends AFTER
.env.localis already in place (Step 3)
- •If you create/modify
- •
Selectors must match actual page source, not assumptions
- •React forms often use
id="name"notname="name"→ use#name - •Step 5 now requires reading actual page source before writing test
- •React forms often use
- •
Test must handle empty database (no existing records)
- •If a dropdown has no options (empty DB), the test must use "create new" flow
- •Otherwise the bug-triggering code path is never exercised
- •
Step 10 (post to issue) is NOT optional — no "benchmark mode" excuse
- •Old Step 10 used local file paths (
/tmp/...) in the GitHub comment — images invisible on GitHub - •Fix: Step 10 now copies screenshots to
.claude/evidence/issue-{N}/, commits, pushes, then posts comment withraw.githubusercontent.comURLs - •Gate check 10 now verifies: (a) images in repo, (b) comment posted, (c) comment uses GitHub URLs not local paths
- •Old Step 10 used local file paths (