Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/agents/code-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 範圍

Expand Down
10 changes: 8 additions & 2 deletions .claude/agents/engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <progress>" "$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)

Expand All @@ -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,零自由發揮**:
Expand Down
19 changes: 12 additions & 7 deletions .claude/agents/spec-writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@ maxTurns: 30

## Design-led 模式(流程起點)

使用者在 Claude design(claude.ai/designclaude.com/...)完成 UI,給你設計稿網址。該網址是**前端 UI 的 source of truth**。
使用者在 Claude design(claude.ai/designclaude.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 "<design-url>"
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 '<button[^>]*>[^<]+</button>' 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 已回答的不重問:
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/tech-lead.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
9 changes: 5 additions & 4 deletions .claude/agents/ui-designer.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,18 @@ 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 '<button[^>]*>[^<]+</button>' 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 '<button[^>]*>[^<]+</button>' $SRC
gh issue view {design_issue_number} --json number,title,body
cat specs/tech-survey.md
```
2. **抽 design tokens**:直接用 design 內看到的數值,不自己決定色票長相;命名色抽出對應 hex。缺色票/狀態記 `design/open-questions.md`。產出 `design/tokens/{colors,typography,spacing}.json`(範本 kit §2)。
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/` 範圍)。

Expand Down
2 changes: 2 additions & 0 deletions .claude/agents/verifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)。
Expand Down
29 changes: 21 additions & 8 deletions .claude/scripts/contract-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 字串
Expand All @@ -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
Expand All @@ -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

Expand Down
38 changes: 25 additions & 13 deletions .claude/scripts/frontend-scaffold.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,27 @@ set -e
ISSUE_NUM="$1"
[ -z "$ISSUE_NUM" ] && { echo "Usage: $0 <issue_num>" >&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",
Expand Down Expand Up @@ -49,7 +61,7 @@ cat > dev/package.json <<'EOF'
}
EOF

cat > dev/tsconfig.json <<'EOF'
cat > "$WEB_DIR"/tsconfig.json <<EOF
{
"compilerOptions": {
"target": "ES2022",
Expand All @@ -60,13 +72,13 @@ cat > 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({
Expand All @@ -75,7 +87,7 @@ export default defineConfig({
});
EOF

cat > dev/index.html <<'EOF'
cat > "$WEB_DIR"/index.html <<'EOF'
<!doctype html>
<html lang="zh-Hant">
<head>
Expand All @@ -90,7 +102,7 @@ cat > dev/index.html <<'EOF'
</html>
EOF

cat > dev/src/main.tsx <<EOF
cat > "$WEB_DIR"/src/main.tsx <<EOF
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
Expand All @@ -101,20 +113,20 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
);
EOF

cat > dev/src/App.tsx <<EOF
cat > "$WEB_DIR"/src/App.tsx <<EOF
// [WIP] Stub — implementation in progress (Refs #${ISSUE_NUM})
export default function App() {
return <div className="p-8">WIP — frontend MVP scaffolding</div>;
}
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}'],
Expand All @@ -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

Expand Down
8 changes: 7 additions & 1 deletion .claude/scripts/local-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading
Loading