diff --git a/.claude/agents/code-review.md b/.claude/agents/code-review.md index 14762de..a486f77 100644 --- a/.claude/agents/code-review.md +++ b/.claude/agents/code-review.md @@ -33,7 +33,9 @@ sprint-test BDD 全綠 → **code-review(你)→** verifier → 關 mileston - `specs/logs/sprint-{N}-review.md` — 結構化 review 報告 - GitHub bug issues(CRITICAL 等級才建) - sprint issue 留言摘要 -- **原則**:只讀不寫,唯讀 sonnet model +- **原則**:只讀產品碼、不改 code(sonnet model);但**報告檔必須寫**(見下) + +> 🛑 **報告檔優先(第一個動作)**:用 Write 先建 `specs/logs/sprint-{N}-review.md` 骨架(各維度章節 + verdict placeholder),**再**邊查邊用 Edit 填。**報告檔就是交付物——只調查不寫報告 = 失敗**。每個維度抽查 2-3 個代表性檔即可,不要逐檔深挖到耗盡 turn;寧可粒度粗也要寫出 verdict 與 bug issue 清單。 ## Review 範圍 diff --git a/.claude/agents/engineer.md b/.claude/agents/engineer.md index 02e4c59..a467d5f 100644 --- a/.claude/agents/engineer.md +++ b/.claude/agents/engineer.md @@ -42,12 +42,14 @@ isolation: worktree | **1** | 認領 issue 後**第一個 Bash 必須是** `deliver-first.sh`(見下,draft PR 落 GitHub)| | **2** `[frontend]` | **第二個 Bash 必須是** `frontend-scaffold.sh "$ISSUE_NUM"`(建 Vite+React+TS+Tailwind 骨架 + commit + push,**不跑 npm install**)| | **3** `[frontend]` | 讀 `specs/design-source/` 本地快照(見下 pixel-perfect gate)| -| **4** | 讀 issue + spec:`gh issue view {n} --json body`;`cat specs/features/f{N}-*.md overview.md dependencies.md`。bug 額外讀失敗 scenario + 重現步驟 | +| **4** | 讀 issue + spec:`gh issue view {n} --json body`;`cat specs/features/f{N}-*.md specs/overview.md specs/dependencies.md`。bug 額外讀失敗 scenario + 重現步驟 | | **5** | 實作(`dev/` 下):依 API contract / bug 描述,遵循 overview.md 架構 + 既有風格;寫 unit tests;維護 compose;自驗所有 AC。**每 ~10 檔跑 `commit-progress.sh "feat: " "$ISSUE_NUM"` push 一次** | | **6** | push 前 / merge 前跑 `bash .claude/scripts/local-checks.sh`(unit + contract,任一紅不准 ready)| | **7** | 收尾**只准跑** `bash .claude/scripts/ready-and-merge.sh "$DRAFT_PR"`(ready→等 CI→squash merge 三合一;CI 紅會 exit 1 讓你去修)→ issue 回報(kit §4)→ loop 下一個 issue | > **npm install / 完整 build verify 留到 Step 6/7 或交給 CI,不要在 Step 1-4 之間跑**(v3/v5 frontend 撞 cap 主因)。 +> **docker 自驗(kit §3 的 `docker compose up` + curl health)是 Step 5 開發期的選配**,不是收尾 gate:本機沒起 DB 也沒關係,整合留給 sprint e2e。收尾只跑 `local-checks.sh`(unit+contract)+ `ready-and-merge.sh`,**別為了「確認服務能起」在 push 前補跑 docker**(與「收尾禁止先搞懂」一致)。 +> **turn 將盡時優先保 deliverable**:loop 清空 lane 時若 turn 快用完,先把當前 issue 的進度 `commit-progress.sh` push 上去(draft PR 不會丟),再停,不要硬撐做完整個 issue 而讓未 push 的工作隨 cap 蒸發。 ### Step 1 🛑 deliver-first(所有 lane) @@ -67,12 +69,16 @@ helper 內部把 fetch+rebase → 開 branch → empty commit → push + 開 dra ```bash [ ! -f specs/design-source/index.html ] && { echo "🔴 design 快照不存在,請 spec-writer 跑 sync-design.sh"; exit 1; } +head -20 specs/design-source.md # 看攝取格式:bundle=元件在 *.jsx / html=在 index.html gh issue view "$ISSUE_NUM" --json body --jq .body | sed -n '/^## Design Reference/,/^## /p' -grep -A 30 'data-testid="sent-record-card"' specs/design-source/index.html # 找對應元件 +grep -rn -A 20 'data-testid="sent-record-card"' specs/design-source/ # 找對應元件(bundle 落在 *.jsx) ls specs/design-source/screenshots/ # Read tool 可直接讀 PNG 對照視覺 cat specs/contracts/dom.md specs/contracts/ux-text.md specs/contracts.ts ``` +> **bundle 格式**(`specs/design-source.md` 標 `攝取格式: bundle`):元件結構 / 字串在 `specs/design-source/*.jsx`,`index.html` 只是 babel loader 殼——grep `*.jsx` 不是 index.html。設計意圖另見 `specs/design-source/chats/`。 +> **testid 來源**:design prototype **通常沒有 `data-testid`**,別預期能從 design grep 到。testid 一律以 `specs/contracts.ts`(TESTIDS)為準——你實作時把這些 `data-testid` **加到**元件上(design 提供的是視覺/結構/文字,testid 是合約新建的)。 + **不 WebFetch 線上 URL** — 本地快照是 sprint 的 SoT。發現過舊 → 找 spec-writer 重跑 sync-design.sh,不自己決定要不要重抓。 **完全照 design,零自由發揮**: diff --git a/.claude/agents/spec-writer.md b/.claude/agents/spec-writer.md index bab6c21..bb9270a 100644 --- a/.claude/agents/spec-writer.md +++ b/.claude/agents/spec-writer.md @@ -12,19 +12,24 @@ maxTurns: 30 ## Design-led 模式(流程起點) -使用者在 Claude design(claude.ai/design 或 claude.com/...)完成 UI,給你設計稿網址。該網址是**前端 UI 的 source of truth**。 +使用者在 Claude design(claude.ai/design、claude.com/... 或 api.anthropic.com handoff bundle)完成 UI,給你設計稿網址。該網址是**前端 UI 的 source of truth**。 1. **下載快照**(不每次 WebFetch — 線上會被改,要 freeze;engineer 直接 grep 本地;截圖供視覺對照): ```bash - bash .claude/scripts/sync-design.sh "https://claude.ai/design/xxx" + bash .claude/scripts/sync-design.sh "" + head -20 specs/design-source.md # 看攝取格式:bundle / html ``` - 產出 `specs/design-source.md`(元數據)+ `specs/design-source/{index.html,screenshots/,assets/}`。 + 產出 `specs/design-source.md`(元數據)+ `specs/design-source/`。新版 handoff bundle 會多出 `*.jsx`(元件原始碼)、`chats/`(設計對話)、`BUNDLE-README.md`。 2. **從本地快照反推 spec**(只讀本地,不打網路): - ```bash - grep -oE 'data-testid="[^"]+"' specs/design-source/index.html | sort -u - grep -oE ']*>[^<]+' specs/design-source/index.html - ``` + - **bundle 格式**:**先讀 `specs/design-source/chats/`** — 使用者與設計助手的完整來回,需求意圖在這裡,別跳過。再讀 `index.html` + 跟著它 import 的 `*.jsx` 理解頁面/元件/資料模型/面向使用者字串。AC 的 selector 用 `{TESTIDS.xxx}` placeholder(testid 由後續 contract phase 發明,prototype 通常沒有,grep 到 0 是正常的): + ```bash + grep -rhoE 'data-testid="[^"]+"' specs/design-source/*.jsx | sort -u # 多半為空 → testid 留給 contract phase + ``` + - **單頁 html 格式**: + ```bash + grep -oE 'data-testid="[^"]+"' specs/design-source/index.html | sort -u + ``` design 提供「前端看到什麼」,但**不會告訴你**:API path/method/schema、data model、業務規則與邊界、認證/權限、錯誤處理、非同步/即時通訊。用 AskUserQuestion 補完這些缺口。 3. **只問 design 看不出來的**。design 已回答的不重問: diff --git a/.claude/agents/tech-lead.md b/.claude/agents/tech-lead.md index 0a3715d..41f4c34 100644 --- a/.claude/agents/tech-lead.md +++ b/.claude/agents/tech-lead.md @@ -39,7 +39,7 @@ echo "✅ Sprint $SPRINT_NUM scope: $FIDS" `specs/contracts/{api,dom,ux-text}.md` + `specs/contracts.ts` 是**所有 lane 的 single source of truth**。沒先有 contract,engineer/qa 同時動手必出現 testid/path/字串互不對齊的災難。範本見 kit §3。 -> **Design-led 專案特殊規則**:若 `specs/design-source.md` 存在,`contracts/dom.md` 與 `ux-text.md` **直接抄自 ui-designer handoff**(`design/components-handoff.md` → dom.md;`design/ux-text-handoff.md` → ux-text.md)。不自創 testid/UI 字串;handoff 有缺漏先回去找 ui-designer 補。`contracts/api.md` 仍由 tech-lead 設計(API 在 design 上看不到)。 +> **Design-led 專案特殊規則**:若 `specs/design-source.md` 存在,`contracts/dom.md` 與 `ux-text.md` **直接抄自 ui-designer handoff**(`design/components-handoff.md` → dom.md;`design/ux-text-handoff.md` → ux-text.md)。**testid**:ui-designer 已負責「design 有就沿用、沒有(prototype 常態)就依元件結構發明」,你照單吸收即可,不另起一套命名跟它打架。**UI 字串**:必須忠實照 design,不自創。handoff 有缺漏先回去找 ui-designer 補。`contracts/api.md` 仍由 tech-lead 設計(API 在 design 上看不到)。 寫完 contract 後 commit,**必須 push 到 origin/main 並自驗成功**(hard rule — v5 曾因 push 被擋沒 retry,導致 per-lane clone 抓不到 contract): diff --git a/.claude/agents/ui-designer.md b/.claude/agents/ui-designer.md index 15d43e8..6a7b557 100644 --- a/.claude/agents/ui-designer.md +++ b/.claude/agents/ui-designer.md @@ -71,10 +71,10 @@ Bash stderr `setValueForKeyFakeAssocArray` / `_encode` 雜訊忽略。「無進 1. **讀 design source + issue**(不 WebFetch 線上 — 本地快照是 SoT): ```bash [ ! -f specs/design-source/index.html ] && { echo "🔴 design 快照不存在,請 spec-writer 先跑 sync-design.sh"; exit 1; } - cat specs/design-source.md - grep -oE 'data-testid="[^"]+"' specs/design-source/index.html | sort -u - grep -oE 'style="[^"]*color:[^;"]+' specs/design-source/index.html - grep -oE ']*>[^<]+' specs/design-source/index.html + head -20 specs/design-source.md # 看攝取格式:bundle=元件在 *.jsx / html=在 index.html + SRC=$(ls specs/design-source/*.jsx 2>/dev/null || echo specs/design-source/index.html) + grep -rhoE 'data-testid="[^"]+"' $SRC | sort -u # 設計若已含 testid 才抽得到(prototype 常為 0) + grep -rhoE ']*>[^<]+' $SRC gh issue view {design_issue_number} --json number,title,body cat specs/tech-survey.md ``` @@ -82,6 +82,7 @@ Bash stderr `setValueForKeyFakeAssocArray` / `_encode` 雜訊忽略。「無進 3. **建元件規格**:每元件 `design/components/{name}/{spec.md,example.tsx}`(格式 kit §3)。 4. **建頁面 layout**:`design/pages/`(kit §4)。 5. **寫 contract handoff**:`design/components-handoff.md`(testid)+ `design/ux-text-handoff.md`(字串)。 + > **testid 來源規則**:Claude Design prototype **通常沒有 `data-testid`**(上面 grep 為 0 很正常)。此時你**依元件結構命名/發明 testid**(kebab-case,對應你在 design 看到的元件,如 `leave-request-card`、`approve-button`、`balance-summary`),這是**團隊新建的測試合約**,不是從 design 抽取。handoff 列出 testid ↔ 元件 對照,frontend engineer 實作時把這些 `data-testid` 加到元件上、qa 用同一份。design 若**已含** testid 則直接沿用、不改名。**字串**一律忠實照搬 design(這個必須抽取,不可發明)。 6. **改 draft → ready + auto-merge + issue 回報**(kit §5:`gh pr ready` → CI 過 → merge)。 7. **review**:sprint-end code-review 一次性處理(**無 per-PR review**);若 review 產出需改動 → 變 issue,由 ui-designer 再認領處理(仍在 `design/` 範圍)。 diff --git a/.claude/agents/verifier.md b/.claude/agents/verifier.md index 629d9f1..770bd27 100644 --- a/.claude/agents/verifier.md +++ b/.claude/agents/verifier.md @@ -10,6 +10,8 @@ maxTurns: 20 檢查指令、報告/日誌範本、收尾流程在 **`.claude/shared/kits/verifier-kit.md`**,通過 hard gate 後才 Read。 +> 🛑 **報告檔優先(過 hard gate 後第一個動作)**:用 Write 先建 `specs/verify-sprint-{N}.md` 骨架(三維度章節 + verdict placeholder),**再**邊查邊用 Edit 填。**報告檔就是交付物——只調查不寫報告 = 失敗**。每個維度抽查 2-3 個代表性檔即可,不要逐檔深挖到耗盡 turn;寧可粒度粗也要寫出 verdict。 + ## 🚦 Hard gate(先做,沒過直接 short-circuit) 驗證前**必須確認最近一次「Sprint E2E Test」workflow 為 success**。e2e 在 GitHub Actions 跑,唯一可信訊號是 workflow conclusion(不是 state.json)。 diff --git a/.claude/scripts/contract-check.sh b/.claude/scripts/contract-check.sh index 574f6c5..a86b236 100755 --- a/.claude/scripts/contract-check.sh +++ b/.claude/scripts/contract-check.sh @@ -30,16 +30,19 @@ fi if [ "$MODE" = "--diff" ]; then FILES=$(git diff --name-only "$BASE...HEAD" -- 'dev/**' 'test/**' | grep -E '\.(ts|tsx|js|jsx|go)$' || true) else - FILES=$(find dev test -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.go' \) 2>/dev/null || true) + # prune node_modules/dist/build/coverage/.next — 否則第三方 .d.ts 的 data-testid 範例會被當違規(誤報上百行) + FILES=$(find dev test \ + \( -name node_modules -o -name dist -o -name build -o -name coverage -o -name .next \) -prune -o \ + -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.go' \) -print 2>/dev/null || true) fi [ -z "$FILES" ] && { echo "ℹ️ 沒有要檢查的檔案"; exit 0; } VIOLATIONS=0 -report() { - echo "❌ $1" - VIOLATIONS=$((VIOLATIONS + 1)) -} +# 注意:report() 過去被放在 `echo|while` 的 subshell 裡,VIOLATIONS++ 不會回傳主 shell, +# 導致 check 永遠 exit 0(從不擋 PR)。改為各 check 區塊在主 shell 用 add_violations 累加。 +report() { echo "❌ $1"; } +add_violations() { VIOLATIONS=$((VIOLATIONS + $(printf '%s\n' "$1" | grep -c .))); } # 1. Hardcoded testid 字面值(僅 .ts/.tsx 檢查;.spec.ts、.feature 寫的 step 也檢查) echo "🔍 Check 1: hardcoded data-testid" @@ -48,17 +51,19 @@ if [ -n "$HITS" ]; then echo "$HITS" | while IFS= read -r line; do report "$line ← 改 data-testid={TESTIDS.xxx}" done + add_violations "$HITS" fi # 2. Hardcoded /api/ 路徑字串(fetch / axios / mux.HandleFunc 等) echo "🔍 Check 2: hardcoded /api/ paths" -HITS=$(echo "$FILES" | xargs grep -nE '"/api/[a-zA-Z0-9/_:-]+"' 2>/dev/null \ +HITS=$(echo "$FILES" | xargs grep -nE '["'"'"']/api/[a-zA-Z0-9/_:-]+["'"'"']' 2>/dev/null \ | grep -vE '(specs/contracts\.ts|specs/contracts/api\.md)' \ | grep -vE '//\s*allow-hardcoded-path' || true) if [ -n "$HITS" ]; then echo "$HITS" | while IFS= read -r line; do report "$line ← 改 API_PATHS.xxx" done + add_violations "$HITS" fi # 3. Hardcoded toast / 中文 UI 字串 @@ -70,6 +75,7 @@ if [ -n "$HITS" ]; then echo "$HITS" | while IFS= read -r line; do report "$line ← 改 toast.xxx(TOAST.yyy)" done + add_violations "$HITS" fi # 4. Playwright locator 中 hardcoded testid @@ -81,18 +87,25 @@ if [ -n "$HITS" ]; then echo "$HITS" | while IFS= read -r line; do report "$line ← 改 page.locator(\`[data-testid=\"\${TESTIDS.xxx}\"]\`) 或 getByTestId(TESTIDS.xxx)" done + add_violations "$HITS" fi # 5. 偵測新增 testid / api path 是否同步更新 contracts.ts if [ "$MODE" = "--diff" ]; then echo "🔍 Check 5: new contract entries synced to contracts.ts" CONTRACTS_CHANGED=$(git diff --name-only "$BASE...HEAD" -- 'specs/contracts.ts' 'specs/contracts/' | wc -l | tr -d ' ') - NEW_TESTIDS=$(git diff "$BASE...HEAD" -- 'dev/**' 'test/**' | grep -E '^\+.*data-testid=' | wc -l | tr -d ' ') - NEW_PATHS=$(git diff "$BASE...HEAD" -- 'dev/**' 'test/**' | grep -E '^\+.*"/api/' | wc -l | tr -d ' ') + # 只算「新增的 hardcoded literal」(同 Check 1/2 的 pattern),排除消費 contract 的插值 + # (data-testid={TESTIDS.x} / `[data-testid="${TESTIDS.x}"]` / API_PATHS.x)。 + # 否則 frontend/qa 正確使用 contract 的 PR 會被誤判成「新增 contract 卻沒改 contracts.ts」。 + NEW_TESTIDS=$(git diff "$BASE...HEAD" -- 'dev/**' 'test/**' | grep -E '^\+' \ + | grep -E 'data-testid="[a-z][a-z0-9-]*"' | grep -vE 'TESTIDS\.' | wc -l | tr -d ' ') + NEW_PATHS=$(git diff "$BASE...HEAD" -- 'dev/**' 'test/**' | grep -E '^\+' \ + | grep -E '["'"'"']/api/[a-zA-Z0-9/_:-]+["'"'"']' | grep -vE 'API_PATHS' | wc -l | tr -d ' ') TOTAL_NEW=$((NEW_TESTIDS + NEW_PATHS)) if [ "$TOTAL_NEW" -gt "0" ] && [ "$CONTRACTS_CHANGED" = "0" ]; then report "PR 新增了 $TOTAL_NEW 處 testid/path,但 specs/contracts.ts 或 specs/contracts/*.md 都沒動。任何新 contract entry 必須先寫進 contracts.ts。" + VIOLATIONS=$((VIOLATIONS + 1)) fi fi diff --git a/.claude/scripts/frontend-scaffold.sh b/.claude/scripts/frontend-scaffold.sh index 596b39c..a756473 100755 --- a/.claude/scripts/frontend-scaffold.sh +++ b/.claude/scripts/frontend-scaffold.sh @@ -13,15 +13,27 @@ set -e ISSUE_NUM="$1" [ -z "$ISSUE_NUM" ] && { echo "Usage: $0 " >&2; exit 2; } -mkdir -p dev/src/{components,pages,api,lib} dev/public +# 決定前端目錄:若 dev/ 已有「非前端」package.json(backend 同居一個 dev/), +# scaffold 進 dev/web/ 隔離,**絕不覆蓋 backend 的 package.json / tsconfig.json**。 +# 純前端專案(dev/ 無 package.json 或本來就是 react)維持 dev/ 根(向後相容)。 +if [ -f dev/package.json ] && ! grep -q '"react"' dev/package.json; then + WEB_DIR="dev/web" + SPECS_REL="../../specs/contracts.ts" + echo "ℹ️ 偵測到 dev/ 已有 backend package.json → 前端 scaffold 隔離到 dev/web/(不覆蓋 backend)" +else + WEB_DIR="dev" + SPECS_REL="../specs/contracts.ts" +fi + +mkdir -p "$WEB_DIR"/src/{components,pages,api,lib} "$WEB_DIR/public" # 已有骨架就 skip(idempotent) -if [ -f dev/package.json ] && [ -f dev/vite.config.ts ]; then - echo "(scaffold 已存在,skip)" +if [ -f "$WEB_DIR/package.json" ] && [ -f "$WEB_DIR/vite.config.ts" ]; then + echo "(scaffold 已存在於 $WEB_DIR,skip)" exit 0 fi -cat > dev/package.json <<'EOF' +cat > "$WEB_DIR"/package.json <<'EOF' { "name": "leave-frontend", "version": "0.1.0", @@ -49,7 +61,7 @@ cat > dev/package.json <<'EOF' } EOF -cat > dev/tsconfig.json <<'EOF' +cat > "$WEB_DIR"/tsconfig.json < dev/tsconfig.json <<'EOF' "esModuleInterop": true, "skipLibCheck": true, "lib": ["ES2022", "DOM"], - "paths": { "@contracts": ["../specs/contracts.ts"] } + "paths": { "@contracts": ["${SPECS_REL}"] } }, "include": ["src"] } EOF -cat > dev/vite.config.ts <<'EOF' +cat > "$WEB_DIR"/vite.config.ts <<'EOF' import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ @@ -75,7 +87,7 @@ export default defineConfig({ }); EOF -cat > dev/index.html <<'EOF' +cat > "$WEB_DIR"/index.html <<'EOF' @@ -90,7 +102,7 @@ cat > dev/index.html <<'EOF' EOF -cat > dev/src/main.tsx < "$WEB_DIR"/src/main.tsx < dev/src/App.tsx < "$WEB_DIR"/src/App.tsx <WIP — frontend MVP scaffolding; } EOF -cat > dev/src/index.css <<'EOF' +cat > "$WEB_DIR"/src/index.css <<'EOF' @tailwind base; @tailwind components; @tailwind utilities; EOF -cat > dev/tailwind.config.js <<'EOF' +cat > "$WEB_DIR"/tailwind.config.js <<'EOF' /** @type {import('tailwindcss').Config} */ export default { content: ['./index.html', './src/**/*.{ts,tsx}'], @@ -123,7 +135,7 @@ export default { }; EOF -cat > dev/postcss.config.js <<'EOF' +cat > "$WEB_DIR"/postcss.config.js <<'EOF' export default { plugins: { tailwindcss: {}, autoprefixer: {} } }; EOF diff --git a/.claude/scripts/local-checks.sh b/.claude/scripts/local-checks.sh index 71d9c03..0dfe4f1 100755 --- a/.claude/scripts/local-checks.sh +++ b/.claude/scripts/local-checks.sh @@ -35,7 +35,13 @@ run_unit() { run_contract() { log "Contract check (grep)" - bash .claude/scripts/contract-check.sh || fail "Contract 違規" + # 用 --diff 只查「本分支相對 origin/main 的改動」,不是全 repo。 + # 否則修了 contract-check 的 subshell bug 後,別的 lane 的既有 contract 債會卡死你的 push。 + if git rev-parse --verify -q origin/main >/dev/null 2>&1; then + bash .claude/scripts/contract-check.sh --diff origin/main || fail "Contract 違規(本分支改動)" + else + bash .claude/scripts/contract-check.sh || fail "Contract 違規" + fi } run_e2e() { diff --git a/.claude/scripts/ready-and-merge.sh b/.claude/scripts/ready-and-merge.sh index 996749a..2c6fe2e 100755 --- a/.claude/scripts/ready-and-merge.sh +++ b/.claude/scripts/ready-and-merge.sh @@ -22,6 +22,14 @@ gh pr ready "$PR" 2>/dev/null || true # 等 CI。沒有任何 check 時 gh pr checks 回非 0("no checks reported"),視為通過 CHECKS_OUT=$(gh pr checks "$PR" --watch 2>&1 || true) if echo "$CHECKS_OUT" | grep -qiE 'fail|error'; then + # 區分「GitHub 帳務/基礎設施擋住、job 根本沒跑」vs「測試真的失敗」。 + # 帳務擋住時 job 會在數秒內 fail(startup_failure),不該當成程式失敗、也不該靜默 merge。 + if echo "$CHECKS_OUT" | grep -qiE 'spending limit|billing|payment|startup_failure|blobnotfound|has not been run|will not be run'; then + echo "⚠️ CI 疑似被 GitHub 帳務 / 基礎設施擋住(非程式失敗),無法判定 PASS/FAIL。" >&2 + echo " 不自動 merge。請確認 GitHub Actions 額度後重跑 CI,或人工 \`gh pr merge $PR --squash --admin\` 覆蓋。" >&2 + echo "$CHECKS_OUT" | grep -iE 'fail|error|spending|billing' >&2 + exit 2 # 與 exit 1(真失敗)區分,讓 caller / 人類知道是 infra 不是 code + fi echo "🔴 CI 失敗,不 merge:" >&2 echo "$CHECKS_OUT" | grep -iE 'fail|error' >&2 exit 1 diff --git a/.claude/scripts/selftest.sh b/.claude/scripts/selftest.sh index 19c5ca0..6f432d7 100755 --- a/.claude/scripts/selftest.sh +++ b/.claude/scripts/selftest.sh @@ -37,13 +37,25 @@ for s in "$SC"/*.sh; do done [ "$SYN_OK" -eq 1 ] && ok "全部 .sh 腳本語法正確($(ls "$SC"/*.sh | wc -l | tr -d ' ') 支)" -# ---- 4. contract-check.sh 能擋 hardcoded 違規(specflow 核心防漂移)---- -if [ -f "$SC/contract-check.sh" ]; then - sh -n "$SC/contract-check.sh" 2>/dev/null \ - && ok "contract-check.sh 可執行(contracts 三件套防線)" \ - || bad "contract-check.sh 語法錯誤" +# ---- 4. contract-check.sh 真的會擋 hardcoded 違規(specflow 核心防漂移)---- +# 不只驗語法 — 驗「違規 exit 1 / 乾淨 exit 0」。曾因 VIOLATIONS 在 subshell 累加而永遠 exit 0。 +if [ -f "$SC/contract-check.sh" ] && sh -n "$SC/contract-check.sh" 2>/dev/null; then + CC=$(mktemp -d); mkdir -p "$CC/dev/src" "$CC/specs" + printf 'export const API_PATHS = { me: "/api/v1/me" };\n' > "$CC/specs/contracts.ts" + # 違規檔(單引號 path,舊版漏網)→ 必須 exit 1(用 if 包裹,否則 set -e 會在此中斷) + printf "const x = '/api/v1/x';\n" > "$CC/dev/src/a.ts" + if ROOT="$CC" sh "$SC/contract-check.sh" >/dev/null 2>&1; then RC_BAD=0; else RC_BAD=$?; fi + # 乾淨檔 → 必須 exit 0 + printf "import { API_PATHS } from '../../specs/contracts';\nfetch(API_PATHS.me);\n" > "$CC/dev/src/a.ts" + if ROOT="$CC" sh "$SC/contract-check.sh" >/dev/null 2>&1; then RC_OK=0; else RC_OK=$?; fi + rm -rf "$CC" + if [ "$RC_BAD" -eq 1 ] && [ "$RC_OK" -eq 0 ]; then + ok "contract-check.sh 真的會擋(違規 exit 1 / 乾淨 exit 0,含單引號 path)" + else + bad "contract-check.sh 失效(違規 rc=$RC_BAD 應為 1;乾淨 rc=$RC_OK 應為 0)" + fi else - bad "contract-check.sh 不存在(specflow 防漂移核心)" + bad "contract-check.sh 不存在或語法錯誤(specflow 防漂移核心)" fi # ---- 5. doctor.sh 可跑且不誤殺 ---- diff --git a/.claude/scripts/sync-design.sh b/.claude/scripts/sync-design.sh index 019be21..0220622 100755 --- a/.claude/scripts/sync-design.sh +++ b/.claude/scripts/sync-design.sh @@ -7,18 +7,23 @@ set -eu # 1. WebFetch 每次都打網路、慢、會用掉 token # 2. design URL 可能被使用者更新,下載快照可以 freeze 一個版本 # 3. engineer 開發時直接讀本地檔案,可離線、可 diff、可 grep -# 4. 截圖可以給 visual review / 之後做 pixel diff +# +# 支援兩種來源格式(自動偵測): +# A. 單頁 HTML(舊 claude.ai/design 分享頁)— curl 直接存成 index.html +# B. handoff bundle(新 claude.ai/design「匯出給 coding agent」,api.anthropic.com) +# 實際是 gzip+tar,內含 README + chats/(設計意圖)+ project/(HTML/CSS/JSX 原型 + 截圖) # # 用法: # sync-design.sh # 或 sync-design.sh (從 specs/design-source.md 讀 URL) # # 產出: -# specs/design-source.md 元數據(URL + timestamp + page list) -# specs/design-source/index.html 主 HTML -# specs/design-source/{page}.html 每個 sub-page(如有) -# specs/design-source/screenshots/ 每頁截圖(PNG) -# specs/design-source/assets/ 下載的圖片 / icon +# specs/design-source.md 元數據(URL + timestamp + 檔案清單 + 攝取格式) +# specs/design-source/index.html 主 HTML(bundle 取 project/index.html) +# specs/design-source/*.jsx|*.css bundle 的元件原始碼(testid / 字串 / 結構的真實來源) +# specs/design-source/screenshots/ 截圖(bundle 內建,或 playwright 補拍) +# specs/design-source/chats/ 設計對話(bundle 才有,記錄使用者意圖 — 反推 spec 必讀) +# specs/design-source/assets/ 圖片 / icon URL="${1:-}" OUTDIR="specs/design-source" @@ -26,41 +31,99 @@ META="specs/design-source.md" if [ -z "$URL" ]; then [ ! -f "$META" ] && { echo "🔴 沒給 URL,且 $META 也不存在"; exit 1; } - URL=$(grep -oE 'https://claude\.(ai|com)/[^ )]+' "$META" | head -1) + # 同時支援 claude.ai/com 與 api.anthropic.com 的 design URL + URL=$(grep -oE 'https://(claude\.(ai|com)|api\.anthropic\.com)/[^ )]+' "$META" | head -1) fi [ -z "$URL" ] && { echo "🔴 找不到 design URL"; exit 1; } echo "📥 Sync design from: $URL" +rm -rf "$OUTDIR" mkdir -p "$OUTDIR/screenshots" "$OUTDIR/assets" -# 1. 下載主 HTML -echo "→ Download HTML" -curl -fsSL "$URL" -o "$OUTDIR/index.html" || { - echo "🔴 下載 HTML 失敗(URL 可能過期或需要登入)" - echo " → 使用者需在 Claude design 重新 share 或 export,再給新 URL" +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +# 1. 下載原始 payload(--compressed 處理 transport 層 gzip;payload 本身可能還是 tar.gz) +echo "→ Download payload" +curl -fsSL --compressed "$URL" -o "$TMP/payload" || { + echo "🔴 下載失敗(URL 可能過期或需要登入)" + echo " → 使用者需在 Claude design 重新 share / export,再給新 URL" exit 1 } -# 2. 用 playwright 截圖(如果裝了) -if command -v npx > /dev/null 2>&1 && [ -f test/package.json ]; then - echo "→ Screenshot pages with playwright" - ( cd test && npx playwright screenshot --browser chromium --full-page \ - --wait-for-timeout 3000 \ - "$URL" "../$OUTDIR/screenshots/main.png" 2>&1 | tail -3 ) || \ - echo " ⚠️ 截圖失敗(不阻塞,HTML 已下載可用)" +# 2. 偵測格式:gzip(tar) bundle vs 純 HTML +KIND=$(file -b "$TMP/payload" 2>/dev/null || echo unknown) +INGEST="html" +case "$KIND" in + *gzip*|*"tar archive"*) + # 可能是 .tar.gz 或裸 tar;先嘗試 gunzip,失敗就當裸 tar + if gunzip -c "$TMP/payload" > "$TMP/payload.tar" 2>/dev/null; then + TARFILE="$TMP/payload.tar" + else + TARFILE="$TMP/payload" + fi + if tar -tf "$TARFILE" >/dev/null 2>&1; then + INGEST="bundle" + fi + ;; +esac + +if [ "$INGEST" = "bundle" ]; then + echo "→ 偵測為 handoff bundle(tar)→ 解開" + mkdir -p "$TMP/x" + tar -xf "$TARFILE" -C "$TMP/x" + + # 找 project 目錄(含 index.html 的那層);找不到就用 bundle 根 + PROJ=$(find "$TMP/x" -type d -name project | head -1) + [ -z "$PROJ" ] && PROJ=$(dirname "$(find "$TMP/x" -name index.html | head -1)") + [ -z "$PROJ" ] && { echo "🔴 bundle 內找不到 project/ 或 index.html"; exit 1; } + + # 攤平 project 內容到 OUTDIR(保留 screenshots/ assets/ 子目錄) + cp -R "$PROJ"/. "$OUTDIR"/ 2>/dev/null || true + mkdir -p "$OUTDIR/screenshots" "$OUTDIR/assets" + + # README + chats(設計意圖,spec-writer 反推需求必讀) + README=$(find "$TMP/x" -maxdepth 3 -iname "README.md" | head -1) + [ -n "$README" ] && cp "$README" "$OUTDIR/BUNDLE-README.md" + CHATS=$(find "$TMP/x" -type d -name chats | head -1) + [ -n "$CHATS" ] && { mkdir -p "$OUTDIR/chats"; cp -R "$CHATS"/. "$OUTDIR/chats"/ 2>/dev/null || true; } else - echo "→ Skip screenshot (playwright 未裝);裝完跑:" - echo " cd test && npx playwright install chromium" + echo "→ 偵測為單頁 HTML" + cp "$TMP/payload" "$OUTDIR/index.html" + # 舊格式才需要 playwright 補截圖(bundle 已內建截圖,且新 README 明示不必截圖) + if command -v npx > /dev/null 2>&1 && [ -f test/package.json ]; then + echo "→ Screenshot with playwright" + ( cd test && npx playwright screenshot --browser chromium --full-page \ + --wait-for-timeout 3000 "$URL" "../$OUTDIR/screenshots/main.png" 2>&1 | tail -3 ) || \ + echo " ⚠️ 截圖失敗(不阻塞,HTML 已下載)" + fi fi -# 3. 解析 HTML 抽出所有 sub-page / iframe URL(如有) -SUBPAGES="" -if [ -f "$OUTDIR/index.html" ]; then - SUBPAGES=$(grep -oE 'href="[^"]+"|src="[^"]+"' "$OUTDIR/index.html" \ - | grep -oE 'https?://[^"]+' \ - | grep -v 'cdn\|fonts\|googleapis' \ - | sort -u) +[ -f "$OUTDIR/index.html" ] || { echo "🔴 攝取後仍無 index.html"; exit 1; } + +# 3. 列出元件原始碼 / 截圖 供 metadata +SHOTS=$(find "$OUTDIR/screenshots" -type f 2>/dev/null | sed "s#$OUTDIR/##;s/^/- /") +[ -z "$SHOTS" ] && SHOTS="- (無)" + +# 依攝取格式預先組好分歧的段落(避免 metadata heredoc 內巢狀邏輯) +if [ "$INGEST" = "bundle" ]; then + COMP_LIST=$(find "$OUTDIR" -maxdepth 1 -type f \( -name '*.jsx' -o -name '*.tsx' -o -name '*.css' \) -exec basename {} \; 2>/dev/null | sort | sed 's#^#- `specs/design-source/#;s#$#`#') + SRC_BLOCK="- 元件原始碼(**testid / 面向使用者字串 / 結構的真實來源,grep 這些檔不是 index.html**): +$COMP_LIST +- \`specs/design-source/chats/\` — 設計對話(**spec-writer 反推需求前必讀**,使用者意圖在這裡) +- \`specs/design-source/BUNDLE-README.md\` — 原 bundle 的 coding-agent 指引" + SPEC_STEP="1. **先讀 \`chats/\`** — 使用者與設計助手的完整來回,意圖在這裡,別跳過 +2. 讀 \`index.html\` + 跟著它 import 的 \`*.jsx\`,理解頁面 / 元件 / 資料模型" + UI_FROM="\`*.jsx\` + \`styles.css\`" + UI_TESTID="\`*.jsx\`" + FE_STEP="grep **\`specs/design-source/*.jsx\`**(元件 / 字串 / testid 在這裡,index.html 只是 loader 殼)" +else + SRC_BLOCK="- (單頁 HTML,無額外元件檔)" + SPEC_STEP="1. 從 \`index.html\` 反推使用者流程" + UI_FROM="HTML / CSS" + UI_TESTID="HTML" + FE_STEP="grep \`index.html\` 對照元件結構" fi # 4. 寫 metadata @@ -68,41 +131,42 @@ TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M:%S UTC') cat > "$META" < ⚠️ 這是 Claude design URL 的本地快照。所有 frontend 開發**只**參照本目錄,不要依賴遠端 URL(可能被使用者更新)。 +> ⚠️ 這是 Claude design 的本地快照。所有 frontend 開發**只**參照本目錄,不要依賴遠端 URL(可能被使用者更新)。 > 要刷新請跑:\`bash .claude/scripts/sync-design.sh\` ## Source - **URL**: $URL - **Last synced**: $TIMESTAMP +- **攝取格式**: $INGEST (bundle = 新 handoff tar / html = 舊單頁) - **Local snapshot**: \`specs/design-source/\` ## Files -- \`specs/design-source/index.html\` — 主 HTML(curl 下載) -- \`specs/design-source/screenshots/\` — playwright 截圖(pixel-perfect 對照用) -- \`specs/design-source/assets/\` — 圖片 / icon 等資源 - -## Sub-pages / Resources -$(if [ -n "$SUBPAGES" ]; then echo "$SUBPAGES" | sed 's/^/- /'; else echo "(無)"; fi) +- \`specs/design-source/index.html\` — 主 HTML +$SRC_BLOCK +- \`specs/design-source/screenshots/\` — 截圖(pixel-perfect 對照) +$SHOTS ## How to use -### Frontend engineer -1. 讀 \`specs/design-source/index.html\` 對照元件結構(grep 找你要做的 component) -2. 看 \`specs/design-source/screenshots/main.png\` 對照視覺 -3. 不要 WebFetch 線上 URL(除非本地快照過舊) +### Spec writer(反推需求) +$SPEC_STEP +3. 看不到的部份(backend / data / 業務邏輯)用 AskUserQuestion 補 -### UI designer -- 從 HTML 抽 design tokens(color / spacing / font)寫進 \`design/tokens/\` -- 從 HTML 找 testid / aria-label,整理成 handoff 給 tech-lead 寫進 \`specs/contracts/dom.md\` -- 從 HTML 抽所有面向使用者的字串,寫進 \`design/ux-text-handoff.md\` +### UI designer(抽 tokens / handoff) +- 從 $UI_FROM 抽 design tokens 寫 \`design/tokens/\` +- 從 $UI_TESTID 找 testid / aria-label → handoff 給 tech-lead 寫 \`specs/contracts/dom.md\` +- 抽所有面向使用者字串 → \`design/ux-text-handoff.md\` -### Spec writer -- 從 HTML 反推使用者流程,寫進 \`specs/features/*.feature\` -- 看不到的部份(backend / data)用 AskUserQuestion 補 +### Frontend engineer(pixel-perfect) +1. $FE_STEP +2. 看 \`screenshots/\` 對照視覺 +3. 不要 WebFetch 線上 URL(除非本地快照過舊) EOF echo "" -echo "✅ Design synced to $OUTDIR/" +echo "✅ Design synced to ${OUTDIR}/(格式:${INGEST})" echo " - HTML: $(wc -c < "$OUTDIR/index.html" | tr -d ' ') bytes" -echo " - Screenshot: $(ls "$OUTDIR/screenshots/" 2>/dev/null | wc -l | tr -d ' ') files" +echo " - 元件原始碼: $(find "$OUTDIR" -maxdepth 1 -type f \( -name '*.jsx' -o -name '*.tsx' \) | wc -l | tr -d ' ') 檔" +echo " - Screenshot: $(find "$OUTDIR/screenshots" -type f 2>/dev/null | wc -l | tr -d ' ') files" +echo " - Chats: $(find "$OUTDIR/chats" -type f 2>/dev/null | wc -l | tr -d ' ') files" echo " - Metadata: $META" diff --git a/.claude/shared/kits/engineer-kit.md b/.claude/shared/kits/engineer-kit.md index 28201ff..32bc494 100644 --- a/.claude/shared/kits/engineer-kit.md +++ b/.claude/shared/kits/engineer-kit.md @@ -81,12 +81,21 @@ volumes: { db_data: } `.env.example`:`APP_PORT=3000` `NODE_ENV=development` `DATABASE_URL=postgresql://user:pass@db:5432/app` `DB_USER=user` `DB_PASS=pass` `DB_NAME=app` `DB_PORT=5432`。 -`.gitignore` 必含 `dev/docker-compose.yml`、`dev/.env`。新增依賴服務(DB/Redis/MQ)時更新 example。 +`.env.example`:`APP_PORT` `NODE_ENV` `DATABASE_URL` `DB_*`。`.gitignore` 必含 `dev/docker-compose.yml`、`dev/.env`。新增依賴服務(DB/Redis/MQ)時更新 example。 -驗證: +> 🛑 **Build context vs repo-root contracts(SpecFlow 固有架構,pipeline lane 必讀)**:`dev/` 的 code `import '../specs/contracts'`(或 `../../`),contracts.ts **在 build context 之外**。若 Dockerfile 用 `build: .`(context=`dev/`),容器內 `tsc`/`vite build` 會找不到 contracts.ts 而失敗。**正解**:context 設 repo root,dockerfile 指到子專案: +> ```yaml +> api: +> build: { context: .., dockerfile: dev/Dockerfile } # context=repo root,COPY specs/contracts.ts 進得來 +> ``` +> 並在 Dockerfile `COPY specs/contracts.ts ./specs/`(或整個 specs/)。**單一 app 用上面的 `build: .` 範本即可;一旦 code import 了 repo-root 的 contracts 就必須改 context。** + +> 🛑 **Fullstack 雙子專案**(backend 在 `dev/`、frontend 在 `dev/web/`,見 frontend-scaffold 的隔離邏輯):compose 要起 `db` + `api`(backend)+ `web`(frontend,`vite preview` 或 nginx)三個 service,各自 Dockerfile、各自 healthcheck,`depends_on` 串成 db→api→web 健康鏈。**先 `ls dev/ dev/web/` 勘查實際結構,不照 issue 字面的目錄假設。** +> 🛑 **寫 `.github/workflows/*.yml` 後 push 前必驗 YAML**(local-checks 不驗 workflow 語法):`python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" .github/workflows/x.yml`。壞掉的 workflow YAML 在 GitHub 上才報、或帳務擋住時根本不報,本地先擋掉。 + +驗證(pipeline 收尾建議真的 `docker compose up` 一次,別只信範本): ```bash cd dev && docker compose up -d --build && docker compose ps -docker compose logs app --tail 20 curl -sf http://localhost:3000/health && echo OK || echo FAIL docker compose down ``` diff --git a/.claude/skills/specflow:start/SKILL.md b/.claude/skills/specflow:start/SKILL.md index de660de..43fdaf8 100644 --- a/.claude/skills/specflow:start/SKILL.md +++ b/.claude/skills/specflow:start/SKILL.md @@ -90,9 +90,29 @@ tech-lead: > **Design-led 順序提示**:理想上 ui-designer 應**先於** tech-lead 完成 contract phase(因為 dom.md / ux-text.md 要吸 ui-designer 的 handoff)。orchestrator 可以先開 design issue → ui-designer 完成 handoff → tech-lead 才產 contracts → 才開 feature/qa issues。 +### Phase 3.8:把 specs/ commit 進 main(hard gate — 否則 lane agent 讀不到) + +spec-writer 與 tech-lead 寫的 `specs/`(overview / features / sprints / contracts / contracts.ts / dependencies)若還停在 working tree **未進版控**,worktree-isolated 的 engineer/qa/ui-designer 從 origin clone 後**完全讀不到 spec 與 contract**,整個 sprint 會憑空亂寫。spawn lane 前必須確認 specs/ 已在 main: + +```bash +cd +if [ -n "$(git status --porcelain specs/)" ]; then + git add specs/ + git commit -q -m "docs(spec): sprint 規劃產出(spec + contracts)納入版控" + git push -q origin main || { + # main 受保護無法直推 → 開 PR 合併 + git checkout -b "chore/sprint-specs" && git push -u -q origin chore/sprint-specs + PR=$(gh pr create --title "docs(spec): sprint specs + contracts" --body "spec-writer + tech-lead 產出" --json number --jq .number) + gh pr merge "$PR" --squash --delete-branch && git checkout main && git pull -q + } +fi +# 確認 contract 三件套 + contracts.ts 確實在 main 上(讀不到就 short-circuit) +git cat-file -e "main:specs/contracts.ts" 2>/dev/null || { echo "🔴 specs/contracts.ts 不在 main,lane agent 會讀不到,停"; exit 1; } +``` + ### Phase 3.9:記錄 sprint_base_sha(給 sprint-end review 用) -在啟動 lane 前先把當前 main HEAD 紀錄起來,這是 sprint review 的 diff 起點: +在啟動 lane 前先把當前 main HEAD 紀錄起來,這是 sprint review 的 diff 起點(須在 Phase 3.8 commit 之後): ```bash SPRINT_BASE=$(git rev-parse main) diff --git a/docs/BENCHMARK-2026-05.md b/docs/BENCHMARK-2026-05.md new file mode 100644 index 0000000..7b9a281 --- /dev/null +++ b/docs/BENCHMARK-2026-05.md @@ -0,0 +1,54 @@ +# SpecFlow Benchmark — 2026-05(請假系統) + +用真實 Claude Design「請假系統」MVP 端到端跑瘦身後的 SpecFlow,目的:(1) 驗證瘦身/重構後的 prompt 在真實專案可靠執行;(2) 系統性找出靜態看 prompt 看不出的隱藏缺陷。 + +- **標的**:員工 / 主管 / HR 三角色請假系統,台灣勞基法假別(3700 行 JSX prototype + 設計對話) +- **demo repo**:`gpwork4u/leave-demo`(private) +- **修正去處**:PR #8(瘦身消重)+ PR #9(12 個缺陷修正 + bundle 支援) + +## 驗證結果:5 種 agent 全跑通 + +| Agent | Issue | 產出 | 結果 | +|------|------|------|------| +| spec-writer | — | 讀 `chats/` 反推 13 feature + Epic/Sprint/milestone + 12 MVP 假設 | ✅ | +| tech-lead | — | 從 0-testid prototype 發明 56 testid + contracts.ts + 15 issue 標 lane + wave 分析 | ✅ | +| backend engineer | #3 | Prisma + seed + JWT auth + RBAC + 10 unit tests(contract 全 import) | ✅ PR #19 merged | +| frontend engineer | #9 | 登入 + AppShell + 導覽(pixel-perfect 照 `*.jsx`,testid 從 contract 加) | ✅ PR #20 merged | +| pipeline engineer | #15 | 雙子專案 Docker + compose + CI workflow | ✅ PR #21 merged | +| qa engineer | #17 | 88 個 Playwright e2e test(1:1 對應 88 條 AC,contract 全 import) | PR #22(CI billing 擋,本地 tsc 過) | + +## 12 個缺陷(全已修) + +### 🔴 發現⑤是核心:防漂移閘門從未運作 + +`contract-check.sh` 的 `report()` 在 `echo | while` 的 subshell 內累加 `VIOLATIONS`,計數無法回傳父 shell → **腳本永遠 `exit 0`**。意即 SpecFlow 標榜的 contract 防漂移閘門**從未真正擋過任何 PR**。 + +修好它之後,原本被它「意外抵銷」的三個誤報全部浮現成「會真的擋死 PR」,**必須一起修**否則修了⑤反而讓所有 frontend/qa PR 卡死: + +| # | 缺陷 | 嚴重 | 修正 | +|---|------|------|------| +| ⑤ | contract-check 因 subshell 計數永遠 exit 0 | 🔴 | 主 shell `add_violations` + selftest 升級為「違規 exit 1 / 乾淨 exit 0」功能測試 | +| ⑥ | frontend-scaffold 在共用 `dev/` 時覆蓋 backend package.json/tsconfig | 🔴 | 偵測 backend → 隔離 scaffold 到 `dev/web/` | +| ⑧ | kit compose 範本不支援「contracts 在 build context 外」+ 雙子專案 | 🔴 | context 設 repo root + 雙子專案三服務指引 | +| ⑪ | local-checks contract 用 full mode → 任一 lane 既有債卡死全 lane(⑤延伸) | 🔴 | 改 `--diff origin/main` 只查本分支 | +| ① | sync-design 不吃新版 gzip+tar bundle | 🟡 | 自動偵測 + 解開攤平(見 DESIGN bundle 段) | +| ③ | 「從 design 抽 testid」對無 testid 的 prototype 不成立 | 🟡 | 改為 contract phase 由 ui-designer/tech-lead 發明 | +| ④ | spawn lane 前 specs/ 未 commit → worktree agent 讀不到 | 🟡 | start skill Phase 3.8 hard gate | +| ⑦ | contract-check full 模式掃 node_modules 誤報(⑤延伸) | 🟡 | prune node_modules/dist/build/coverage/.next | +| ⑫ | contract-check Check 5 把模板插值 `[data-testid="${TESTIDS.x}"]` 誤判為新 contract(⑤延伸) | 🟡 | 只算真 literal、排除 TESTIDS./API_PATHS | +| ② | Claude Design export URL 短時效(數分鐘 404) | 🟢 | 文件提示重 export + 本地快照 freeze | +| ⑨ | ready-and-merge 把 CI billing/infra 失敗誤判為程式失敗而 deadlock | 🟢 | 偵測 billing/startup_failure/BlobNotFound → exit 2 | +| ⑩ | 無 workflow YAML lint gate | 🟢 | kit 補 push 前 `yaml.safe_load` | + +外加:engineer.md Step 4 路徑 bug(`overview.md`→`specs/overview.md`)、docker 自驗歧義、contract-check 單引號 path 盲點。 + +## 方法論心得 + +1. **每跑一個沒走過的 lane 就揪出該 lane 專屬 bug**(backend→⑤、frontend→⑥⑦、pipeline→⑧⑨⑩⑪、qa→⑫)。靜態 review prompt 看不出這些,必須真的執行未走過的程式路徑。 +2. **修一個核心 gate 會連鎖暴露被它隱藏的下游缺陷**(⑤→⑦⑪⑫)。修可靠度核心時要追下游交互,不能只修當下那一處。 +3. **token 省幅與可靠度可以雙贏**:消除 prompt/kit 與 helper script 的重複死碼(PR #8)既省 token 又消除漂移風險。 + +## 已知環境限制(非 SpecFlow 問題) + +- demo 用的 GitHub 帳號 Actions 被 billing 擋住(job 數秒 startup_failure / BlobNotFound)→ sprint e2e、code-review、verifier 這後段無法在 CI 跑、#22 無法經 CI merge。⑨ 的修正讓 ready-and-merge 不再因此 deadlock。 +- engineer/qa/ui 用 `isolation: worktree`,完整 multi-agent 跑須在以目標專案為根的 Claude Code session 執行 `/specflow:implement`。