AgentSkillsCN

frontend-bug-verification

利用Git工作树与Playwright,自动进行前端Bug验证。 通过工作树同时运行“有Bug”与“已修复”的环境。 无需远程预演环境的任何操作。强制执行“有Bug”与“已修复”环境的截图对比。

SKILL.md
--- frontmatter
name: frontend-bug-verification
description: |
  Automated frontend bug verification using git worktrees + Playwright.
  Runs BEFORE (buggy) and AFTER (fix) environments simultaneously via worktrees.
  No remote staging manipulation needed. BEFORE/AFTER screenshot enforcement.
user-invocable: true
allowed-tools: [Bash, Read, Write, Grep, Glob]
context: fork

Frontend Bug Verification Skill

原則:GitHub Issue 留言區 = 唯一真相來源。 所有除錯過程(重現、根因、修復、驗證)都必須完整記錄在 Issue 留言區。 對話會消失,Issue 留言不會。任何人隨時都能回顧完整脈絡。

Philosophy: "SCREENSHOT BUGS. Every bug gets a screenshot." - AI QA Engineer 2026

Usage

code
/frontend-bug-verification {N}

Where {N} is the GitHub issue number.


IMMUTABLE RULES (NEVER BYPASS)

Rule 1: NO MERGE / NO PUSH to protected branches

code
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)

code
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

code
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

code
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

code
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

code
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

bash
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:

bash
# 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:

  1. $GATE $ISSUE_NUM {N} check — before starting (blocks if previous steps incomplete)
  2. Do the work
  3. Run the gate check logic
  4. $GATE $ISSUE_NUM {N} pass — after gate check passes (unlocks next step)

Step 1: Read Issue Details

bash
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:

bash
test -s "$EVIDENCE_DIR/issue.json" && echo "GATE 1: PASS" || echo "GATE 1: FAIL"

Step 2: Create Worktrees

bash
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:

bash
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!

bash
# --- 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:

bash
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.

bash
# --- 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:

bash
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.

bash
# 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" not name="name" → use #name selector
  • Select dropdowns may have no options in empty DB → test must handle "new" flow
  • Some forms use controlled components with no HTML name attribute at all

Create frontend/e2e/issue-{N}-before.spec.ts directly in the BUGGY worktree:

typescript
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):

bash
# Write directly to worktree — do NOT copy from main repo
# File: $WORKTREE_DIR/buggy/frontend/e2e/issue-{N}-before.spec.ts

GATE CHECK 5:

bash
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

bash
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):

bash
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:

typescript
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:

bash
cp frontend/e2e/issue-{N}-after.spec.ts "$WORKTREE_DIR/fix/frontend/e2e/"

GATE CHECK 7:

bash
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

bash
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):

bash
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

bash
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.

bash
# --- 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 存在)
![BEFORE](${REPO_URL}/.claude/evidence/issue-${ISSUE_NUM}/${BEFORE_IMG})
- **觀察**:[描述截圖中看到的 bug]

### 3. 根因分析
- **為什麼出錯**:[簡述根因]
- **相關檔案**:\`path/to/file.ts:line\`
- **影響範圍**:[哪些功能受影響]

### 4. 修復內容
- **PR**:#XX
- **改了什麼**:[一句話描述修改]
- **Branch**:\`fix/issue-${ISSUE_NUM}-*\`

### 5. AFTER(Bug 修復)
![AFTER](${REPO_URL}/.claude/evidence/issue-${ISSUE_NUM}/${AFTER_IMG})
- **觀察**:[描述截圖中 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):

bash
# 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

bash
# 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)

bash
# 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.

code
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

ConditionActionMay 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

code
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"

bash
# Another worktree or the main repo has this branch checked out
git worktree list
# Remove stale worktrees
git worktree prune

Port already in use

bash
lsof -i :3000 | grep LISTEN
kill <PID>

Backend needs .env

bash
# 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

typescript
// 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-*.png or 05-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:

  1. .env.local with correct NEXT_PUBLIC_API_URL is REQUIRED per worktree

    • Without it, Next.js defaults to http://localhost:8000 — buggy frontend hits fix backend
    • Step 3 now creates .env.local with port-specific API_URL for each worktree
    • Gate check 3 now verifies the correct port is in each .env.local
  2. Next.js requires restart after .env.local changes

    • If you create/modify .env.local after npm run dev, you must restart the frontend
    • Step 4 should start frontends AFTER .env.local is already in place (Step 3)
  3. Selectors must match actual page source, not assumptions

    • React forms often use id="name" not name="name" → use #name
    • Step 5 now requires reading actual page source before writing test
  4. 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
  5. 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 with raw.githubusercontent.com URLs
    • Gate check 10 now verifies: (a) images in repo, (b) comment posted, (c) comment uses GitHub URLs not local paths