Skip to content
Back to skills

Commit

ASecurity

Use when 使用者要求提交工作區變更、merge back 已完成 worktree,或 worktree 就緒佇列達到提交條件;需要拆成多筆時同樣適用。NOT for 把 clade 改動散播到 consumer(走 /clade-publish),NOT for 新建實作 worktree(走 /wt)。

  • 44 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 5, 2026
ai-agentsshellbashsqlvuenodegitapici/cdsecurity

Works with

  • claude code
  • cursor
  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 30, 2026

npx -y skills add Charles5277/nuxt-supabase-starter --skill commit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Commit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Commit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/charles5277-commit-nuxt-supabase-starter/badge)](https://www.skillsdirectory.com/skills/charles5277-commit-nuxt-supabase-starter)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: commit
description: Use when 使用者要求提交工作區變更、merge back 已完成 worktree,或 worktree 就緒佇列達到提交條件;需要拆成多筆時同樣適用。NOT for 把 clade 改動散播到 consumer(走 /clade-publish),NOT for 新建實作 worktree(走 /wt)。
metadata:
  clade:
    permission_tier: action
---

<!-- clade-skill-scope: both -->

<!-- never-density-reviewed: 2026-09-04 — 全檔 20 條逐條覆核過(不是只看本次新增的)。**這裡刻意不寫行號**:每次編輯本檔行號就漂一次,寫了只會讓下一個覆核的人拿一組對不上的座標去找。以錨點文字定位——(a) Step 2 「丟棄 WIP 的委婉說法」那個 7-run,每一行點名一種不同的說法,合併就失去反開脫作用;(b) 「不要把 WIP 併進來」那條規則掛在兩個不同的 AskUserQuestion 上,各自在使用點才讀得到,不能只留一處;(c) 其餘 13 條各自是該禁令在全檔的唯一出處。Step 6-A 本次新增的 4 條已先收斂掉 3 條重複的「NEVER 再 bump」與 1 條重複的逃生口禁令,剩下的每一條對應一個實測過的失敗模式。-->

## User Input

```text
$ARGUMENTS
```

政策與禁止事項見當前 runtime 已投影的 `commit` 與 `commit.detail` 規約。本檔定義執行流程,commit 類型 / emoji 對照表在 Step 3。

## Step 0-Batch: 提交入口與既有批次(取得 commit lock 前)

**每次**進入 `/commit` 先跑 `wt-helper batch status --workflow <已解析 workflow_model>`(consumer:`scripts/wt-helper.ts`;clade:`vendor/scripts/wt-helper.ts`)。**NEVER** 省略 `--workflow` 讓 CLI 默認 PR 制。使用者主動要求的 trigger 是 `manual`,無最低件數;自動收割用 `auto`:`pr-merge-based` 1 個 distinct work id 即準備獨立 PR,`trunk-based` 仍要 4 個才啟動。dependency/drained/stop 可提前結批。Checkpoint 與 draft 都不是 `/commit`,不得在 checkpoint 或 draft 啟動完整品質鏈或發版。Draft 不是 ready。

Cloud/worker completion 與該 PR 的 CI 通知 MUST 喚醒**同一 coordinator** 續跑本入口,不請 Charles 代觸發。integration 模式的切片沒有自己的 PR/CI:喚醒 coordinator 的是 worker 的 completion,coordinator 併入 `integration/<work-id>` 後,整件工作只對 `main` 跑一次本入口。Coordinator 核對遠端 SHA 後把 commits/證據匯入本機受管來源,再 `batch ready`。Worker 完成 ≠ landing。

有就緒成員或待續跑/待清理批次時 **MUST 讀 [batch.md](batch.md)**,先準備或接續隔離整合區,再在該區跑本 skill 的完整 Step 0–5;Step 5 後 seal/land,正式落地才進 Step 6。PR 制合併前 MUST 先跑 `deploy-trigger-check.ts`(與 Step 6-Gate 同一支),判定 main 更新會不會未授權部署 production。`confirmed-push-main` 且 production 由 main 觸發 → 停止 unattended merge。Unattended 預先核准 `release=manual` 時 Step 6 停在 6-B(已 land、未發版)。已落地只欠 cleanup 的批次直接清理,不重跑品質鏈。沒有就緒成員且沒有 active batch 時走普通 `/commit`,當前 WIP 照常全包。未達自動門檻時返回開發,**不取得 commit lock**。

## Step 0-Lock: 單一 session 防呆(進品質流程必做第一步)

以 `COMMIT_TARGET_ROOT` 保存本次品質流程的工作區絕對路徑;鎖腳本由下列 runtime-lifecycle 解析目前 skill 資源取得。整輪 acquire/release 使用同一組 owner 與路徑,包含切回 main 或清理 integration 之後。

**MUST 先完整讀 [runtime-lifecycle.md](runtime-lifecycle.md)**,取得本工作的 work id、當前 runtime/session 與 checkout,再依「取得與續持」執行 acquire 並保存 owner receipt。相同文件定義每個 gate 邊界的 renew、背景工作的收回、退出與中斷恢復。

取得失敗時停在本步;依當前入口可用且已授權的通道聯絡具名持有者,沒有通道或恢復授權時回報具體缺口。**NEVER** 自行 `rm` 鎖檔繞過。Timeout、失聯與短命 CLI 的 PID 消失都不能授權接管;同 session 重入仍須匹配 work/runtime/session 與原 token。

成功 acquire 只證明持鎖;下列 WIP、品質與發版 gate 仍須逐項完成。

## Step 0-Coord: Cross-Session Staged Pollution Detection

跑三種活動線索(index.lock 年齡、publish sidecar、wt-helper stash),它們是 warn-only 探測。任一命中時先完整讀 [gates.md](gates.md) § 0-Coord,依實際身份與可用且已授權的通道協調,未解決策再詢問使用者。線索全靜默不免除下一步的所有權判定。

批次模式的工作區是 helper 登記的 integration path,全包的是固定成員的完整 base→candidate diff;main 的既有 WIP 保留。以下全 WIP 條款對普通模式與 integration 工作區各自成立。已在本批正式 commit 的內容仍屬 review scope,**NEVER** 因 `git status` 乾淨就省略未完成的批次 gates。

**預設行為**:所有 `git status` 顯示的 uncommitted 變更(含與本次工作無關、其他 session 並行的 WIP、不認得的檔案)**一律無條件**列入本次 `/commit` 流程,照常跑 0-A review、在 Step 3 依功能分組成獨立 commit。

## Step 0-Transport: 取得實際可用的執行載體

先讀 [runtime-gates.md](runtime-gates.md) 的三端載體與逐 gate 證據契約,再依本次 catalog 選執行工具。

每個 gate 使用當前 runtime 的原生操作;完整映射在 [gates.md](gates.md) 末尾的 runtime 操作段。需要 CLI wrapper 時先確認其實際檔案、工具與授權;依 runtime-lifecycle 取得本次載入的 skill 絕對路徑;隨 skill 交付的 CLI 位於該目錄的 `scripts/`。實際派 Pi 時才讀當前已投影的 `agent-routing.pi-watch-protocol`;原生派遣不捏造 Pi receipt。缺能力的 gate 保持未完成,不悄悄換 runtime 或略過品質要求。

## Step 0-Scope: 列候選、查歸屬、保留 WIP

先完整讀當前 runtime 已投影的 `commit.detail`「WIP 處置決策樹」與 `scope-discipline`「歸屬探測前置」。所有 uncommitted 變更先列入候選;使用者在本任務或參數中指定的範圍持續有效。不同主題、來源陌生、想讓 commit 乾淨都不是自行排除或 stash 的理由。

每組實際提交前確認內容歸屬、授權、與活躍寫入者的交接及品質 gate。HEAD 沒有該檔、路徑互不重疊或檔案存在都不能證明 ownership。尚在寫入或歸屬未明的內容保留原狀,依上述同一套探測/協調契約處理;候選清單不等於已授權接管。

WIP 確實阻礙本次工作時,使用 `commit.detail` 的三項 stash predicate,限定具體已授權路徑。建立 stash 後立即釘住 SHA;HANDOFF 記錄 SHA、理由、範圍與按該 SHA 恢復的指引。寫完紀錄才繼續;共享 stash 不用浮動的最新 entry 恢復。

工作檔覆寫與 index-only 操作的效果不同,逐一套用 `commit`/`scope-discipline` 的所有權判準。只讀診斷與文件中的指令引用不執行 Git mutation;已明確授權的局部操作沿原 scope 執行,不因工具名字或缺少另一端的詢問工具重問。丟棄 WIP 仍須使用者對具體對象與動作的明確指示,禁止以「清理」「回到 baseline」代替授權。

完成前置判定後,已觸發的 ceremony 直接繼續分組與品質檢查,不再請使用者重複觸發 `/commit`。

## Step 0-MR: Branch Gate(main / master 限定,硬擋無 override)

本 gate 適用 `main` / `master` 與 helper 登記的 batch integration;其他 feature branch 才 skip。批次 scope 用 base→candidate(已正式 commit 的部分也包含),不能因 branch 名稱或 status 乾淨跳過:

- **0-MR 人工檢查 Gate**:本次 commit 觸及進行中的 work item carrier(`tasks/<date>-<slug>.md` 或 `specs/plans/NNN-<slug>/tasks.md`)時觸發。觸發時 **MUST** 先完整讀 [gates.md](gates.md) § 0-MR 的判定流程、auto-triage 路由表與禁止項再繼續。判定粒度是 **pathspec 交集**:BLOCK 的工作只 withheld 自己 carrier 的路徑,其餘 group 照常走 Step 3 / Step 4(gates.md § 0-MR step 6)。

## Step 0: 品質檢查

先判斷 Step 0-Scope 的本次變更是否命中 [`review-tiers.md`](rules/review-tiers.md)(路徑相對 `$COMMIT_RESOURCE_DIR`,見 [runtime-lifecycle.md](runtime-lifecycle.md) § 執行依賴)
Tier 3;命中才執行官方 Codex Security path scan。觸發時 **MUST** 先完整讀
[gates.md](gates.md) § 0-S 的範圍、成本上限與 exit 分流再繼續。未命中則跳過,進入一般
獨立 code review(唯一合格 reviewer:fresh-context Claude Opus 5.5(effort: medium),`claude-review-safe.sh`;沒有備援席)。完整 repository baseline 保持 operator 明確觸發,不屬於 `/commit`。

### 0-A/B/C/D 執行與匯合

0-A.0 simplify 序跑第一,修完的內容才成為 review snapshot。完成後判 fast-path;不命中則啟動 0-A.1,並行已觸發的 0-B 與 0-C。每個工作都須有實際 handle、owner 與收回方法,完整映射與沒有非同步能力時的處置見 [gates.md](gates.md) 及 [review-policy.md](review-policy.md)。

```text
simplify → fast-path 判定
  → 0-A.1(或合法 skip)/0-B(條件觸發)/0-C
  → 收回結果、匯合修正
  → 0-A.1 有 Critical/Major:修正後 0-A.2 深度 review
  → 0-D → 大改動回扣 → 0-E → 0-F
```

啟動後直接推進沒有資料依賴的軸,不只輪詢。Reviewer 看已凍結的內容;check/fix 軸可以推進,但後續內容不能冒用舊 snapshot 的 PASS。Runner 自帶 before/after 漂移檢查時依其限制選隔離場地,不能因「snapshot 已凍結」就忽略它實際會拒絕 working tree 變動。

**Fast-path 判定**(同時滿足下列三條件才能跳過 pi review,任一不滿足都跑):

1. 整個 diff 行數(additions + deletions)< 20 行
2. 改動限於 doc / config 類檔案:`*.md`、`*.json`(**除** `package.json` 的 `dependencies` / `devDependencies`)、`*.yml`、`*.yaml`、`.gitignore`、`HANDOFF.md`、`ROADMAP.md`
3. 無 sensitive 路徑(依 [`review-tiers.md`](rules/review-tiers.md)(路徑相對 `$COMMIT_RESOURCE_DIR`,見 [runtime-lifecycle.md](runtime-lifecycle.md) § 執行依賴) Tier 3):`**/migrations/**`、`**/auth/**`、`**/permission*`、`**/rls*`、`*.sql`、`**/*security*`

任何 `.ts` / `.tsx` / `.vue` / `.mjs` / `.js` / `.sh` 變更(即使單行)都**不適用** fast-path —— 邏輯 bug 在小 diff 很常見,獨立 review 仍有價值。

**修正後的證據**:每一個實際 gate 都要完成並綁定受測範圍;匯合修正超過 50 行或跨 5 檔以上時,依 gates.md 的大改動回扣驗新 snapshot。問題先收回、匯合一次修,避免多位 writer 同時改同一內容。

### Gate 執行細節

每個 gate 的完整執行流程(bash scripts、trigger 條件、fix loop、pi offload)見 [gates.md](gates.md)。執行任一 gate 前 **MUST** 先讀對應 §。

- **0-A 程式碼審查**:simplify(0-A.0)→ 合格獨立 review(0-A.1)→ Critical/Major 條件觸發深度 review(0-A.2)。唯一合格 reviewer 是 fresh-context Claude Opus 5.5 medium(Claude Code 主線走 `claude-review-safe.sh prepare` → AGENT_CALL → FINALIZE);Opus 不可用時 gate 保持未完成,不以主線自審或其他模型補位。詳見 [gates.md](gates.md) § 0-A。
- **0-B UI Design Review**:已採用 impeccable 且有 UI 檔變更時跑 0-B.1(detector 乾淨、唯讀核對受影響畫面 P0/P1 的處置證據、token 動了要有適用的 DESIGN.md 或說明);未採用跳過。0-B.2 在 `.vue` template 變更且有視覺影響時判讀。詳見 [gates.md](gates.md) § 0-B。
- **0-C CI 等效檢查**:`pnpm check` + `pnpm test` + `pnpm run doctor`,全綠才過。詳見 [gates.md](gates.md) § 0-C。
- **0-D Doc Alignment**:條件觸發(diff 觸及 docs / rules / snippets / audit / 業務碼 / pitfall)。詳見 [gates.md](gates.md) § 0-D。
- **0-E evlog map 覆蓋率**:條件觸發(diff 觸及 entry point:`server/{api,routes,middleware,tasks}/` / pages / Next route handler)。`@evlog/cli` **必裝**(缺裝 = block commit,比照 0-C 的 doctor);本次 diff 觸及的**每一個** entry point 都 MUST 滿分。詳見 [gates.md](gates.md) § 0-E。
- **0-F 最佳實踐交叉比對**:條件觸發(diff 新增 snippet / audit script / skill / rule)。問「既有資產是不是已經涵蓋這件事」與「這次做的該不該登記進去」。advisory,不擋 commit。詳見 [gates.md](gates.md) § 0-F。

### 紀律禁止項

- **NEVER** 在 0-A.1 背景跑且有獨立可執行軸時,主線只 poll 不做事;推進 0-C 與已觸發的 0-B,載體與並行限制依 review-policy 判定。
- **NEVER** 以並行收益省略受測 snapshot 或 completion 證據;只有可隔離、可收回的工作才同時執行,缺能力依 review-policy 明示限制。

其餘紀律禁止項見 [gates.md](gates.md) § 0-A/B/C/D 並行匯合 紀律禁止項。

## Step 1: Schema 同步檢查(條件觸發)

**每一次** `/commit` 都 MUST 跑這一步的觸發判定 —— 判定本身無條件,判定**結果**才決定要不要做事:

```bash
# 範圍=本次落地的變更。普通 /commit:git status(staged/unstaged/untracked 逐檔列),branch 上已 commit 的不算。
# 批次 /commit(helper integration 工作區,branch 固定 codex/batch-<id>)另加 base→candidate tree——member checkpoint
# 已正式 commit、git status 乾淨的 migration 也算。先在該工作區設(clade:vendor/scripts/):
#   BATCH_SCOPE=$(node scripts/wt-helper.ts batch scope) || BATCH_SCOPE='<batch scope 失敗>'
#   (MUST 與下方判定在同一次 Bash 呼叫裡:每次 tool call 都是新 shell,變數帶不過去,分開跑必落 HAS)
# 批次 branch 上 BATCH_SCOPE 沒設、空、helper 失敗或讀不到,一律寧可判 HAS
PAT='supabase/.*\.sql|supabase/migrations/|\.types\.ts'
case "$(git symbolic-ref --short -q HEAD)" in codex/batch-*) IN_BATCH=1 ;; *) IN_BATCH= ;; esac
{
  git status --porcelain --untracked-files=all
  if [ -n "$IN_BATCH" ] || [ -n "${BATCH_SCOPE:-}" ]; then
    printf '%s' "${BATCH_SCOPE:-}" \
      | node -e 'const s=JSON.parse(require("fs").readFileSync(0,"utf8"));if(!s.base||!s.tree)process.exit(1);console.log(s.base,s.tree)' \
      | { read -r B T && git diff --name-only "$B" "$T"; } || echo 'supabase/migrations/<batch scope 讀不到>'
  fi
} | grep -Eq "$PAT" && echo HAS || echo NO
```

- `NO` → 本 repo 這次沒動到 migrations 或 types,**直接進 Step 2**,不需要讀任何東西。
- `HAS` → **MUST** 先完整讀 [schema-sync.md](schema-sync.md) 並照其中 Step 1.1–1.5 **每一步**走完,再進 Step 2。
  Step 1.4(SQL lint)與 1.5(advisors)在 1.3 的 reset 之後跑,**NEVER** 做完 types 比對就當 Step 1 結束。

上面這條判定刻意寬鬆(寧可誤送進 reference 也不漏),精確判定與完整流程都在 reference 檔裡。
判定範圍**只到本次落地**,**NEVER** 加回 `merge-base→HEAD` 全 branch 判定:branch 早期一旦含
migration,之後每次 `/commit` 都會判 HAS、進 Step 1.3 跑破壞性 `db:reset`(desk 共用 canonical DB)。
普通 `/commit` 能略過 branch 上已 commit 的 migration,前提是它們在自己那次 `/commit` 已走過 Step 1。
這個前提對 **checkpoint 不成立**:batch/worktree checkpoint(Step 0-Batch:checkpoint 不是 `/commit`)、
version-upgrade fleet-mode worker 的 scoped checkpoint 都沒跑過 Step 1。它們一律經批次 `/commit` 落地,
由上面 `BATCH_SCOPE` 的 base→candidate 分支涵蓋——批次 `/commit` **NEVER** 省略 `BATCH_SCOPE`;漏設或 helper 失敗時批次 branch 判 HAS,不會退回只看 `git status`。
不經 `/commit` 也不經批次入庫的 stale types 由 CI `supabase-check` 擋(<consumer-a> wr4-002 #34,2026-09-25)。`--untracked-files=all` 不可省:
整個 `supabase/` 目錄都是新檔時,預設只印 `?? supabase/`,pattern 對不到。
**NEVER** 憑印象自行重建重置 / 比對 / lint 流程 —— `pnpm db:reset` 與 `supabase db reset` 的分支、
`cp` 備份先於重置的順序、自訂 `config.dbTypesPath` 的解析,寫錯任一條都會靜默放行不一致的 schema。

## Step 2: 檢查變更狀態

```bash
git status --porcelain          # 分組輸入的權威來源(含 tracked modified + untracked)
git diff --stat                 # 僅輔助看 tracked 改動規模;NEVER 當分組輸入唯一來源
```

> **分組輸入 MUST 用 `git status --porcelain`,不是 `git diff --stat`**:`git diff --stat` **只列 tracked modified**,會漏掉 untracked 非 ignored 檔(`??` 開頭,如新建的 `tasks/todo.md` / `docs/<new>.md`)。只憑 `git diff --stat` 分組 → untracked 檔被 silently 丟掉、never commit。每次分組前自問:「`git status` 有沒有 `??` 開頭的行?沒被 `.gitignore` 覆蓋 = 必須納入分組。」

若 `.gitignore` 有變更:

- **允許保留**:僅新增 Clade 管理的 installation artifact / runtime state ignore 條目(例如 `.claude/.commit.lock`、`codex/`)
- **其他變更** → 回到 Step 0-Scope 查明歸屬與本次授權;需要 stash 時先滿足同一套三項 predicate,限定 `.gitignore` 並釘住 SHA、登記 HANDOFF。**NEVER** 用 `git checkout .gitignore` 直接還原(會毀掉使用者的 WIP)。

## Step 3: 分析變更並分組

依功能/目的分組並輸出:

```text
### Group 1: [功能描述]
類型: feat
檔案:
- path/to/file.ts
```

`類型` 取值與 Step 4 訊息開頭的 emoji(`commitlint.config.ts`):

| Emoji | Type     | 用途     |
| ----- | -------- | -------- |
| ✨    | feat     | 新功能   |
| 🐛    | fix      | Bug 修復 |
| 🧹    | chore    | 維護     |
| 🔨    | refactor | 重構     |
| 🧪    | test     | 測試     |
| 🎨    | style    | 樣式     |
| 📝    | docs     | 文件     |
| 📦    | build    | 建置     |
| 👷    | ci       | CI/CD    |
| ⏪    | revert   | 還原     |
| 🚀    | deploy   | 部署     |
| 🎉    | init     | 初始化   |

**分組輸入 = Step 2 的 `git status --porcelain` 完整輸出**(tracked modified + untracked 非 ignored),**NEVER** 只用 `git diff --stat`。

- **Untracked 非 ignored 檔(`??`)一律納入分組**,通常自成獨立 `chore` group(除非語義明確屬於某 feat / fix group)
- 看到 `??` 開頭的檔想加 `.gitignore` 消掉時 **STOP**:先問「這本來就該 ignore(build artifact / runtime state),還是我在逃避 commit?」逃避 commit 而 gitignore = 把該入庫的東西藏掉,方向反了(詳見 [[wip-orphan-recovery]] § 反射性 gitignore 禁令)
- **0-MR withheld scope 內的路徑不進任何 group**(gates.md § 0-MR step 6 印出的 carrier 路徑):它們留在 working tree,Step 5-A 登記進 HANDOFF。這是「全部變更都要入庫」的機械例外

分組顆粒是本 skill 的**內部決策**:**NEVER** 對別的 session 宣告「我要拆幾筆、哪筆先 land」或拿它協調——對方要等的只有「land 了沒有」這一個布林。

## Step 4: 逐一執行 Commit

對每個分組(用 `git commit --only -- <files>` 強制 limit scope,防別 session staged race — 詳見 `rules/core/commit.md` § Ad-hoc commit 必走 `git commit --only -- <paths>`):

Co-author trailer 依本 repo 已核准的署名政策與實際參與者填寫;有可核對的名稱/email 才加入。沒有署名資料時省略 trailer,不把其他 runtime 的名稱或 email 當預設。下列一般與 deploy commit 範本共用此判準。

```bash
git commit --only -m "$(cat <<'EOF'
✨ feat: 功能描述

Via: /commit
EOF
)" -- <files>
git log -1 --oneline
git show --stat HEAD | tail -3   # MUST verify scope == expected files
```

Untracked file 例外:須先 `git add <untracked>` 再 `git commit --only -- <both-paths>` — scope 仍受 `--only` 過濾。

0-MR 有 withheld scope 時,**每個 group 的 `git commit --only` 之前 MUST** 先跑(gates.md § 0-MR step 6):

```bash
node ~/offline/clade/vendor/scripts/commit-mr-gate.ts intersect --block <X> [--block <Y>] -- <files>
```

exit 1 → stdout 列的路徑落在 withheld scope,該 group **NEVER** commit;移出那些路徑後重跑,剩餘才 commit。

## Step 5: 更新 HANDOFF.md 與 ROADMAP

依 `follow-up-register.md` § 主動消化,同步驗證並關閉本次完成的 TD,回讀 flow 關卡後移出主清單;HANDOFF 移除完成流水帳,已有 TD 的未完項只保留指針。等待訊號、部分完成及未驗收工作保留具體接手入口。

遵守當前 runtime 已投影的 `handoff`:Step 4 分組 commit 完成後**必須**更新 `HANDOFF.md`,把**所有可延續且尚未被接手的後續工作**寫入 —— 不限於當前這件。同時同步 repo 根目錄 `ROADMAP.md` 的 `## Next Moves`;判定與缺能力處置見 handoff-steps.md。

> 本步驟在 Step 6 **之前**執行——HANDOFF/ROADMAP 的 commit 會跟 Step 6-A 的 bump/deploy commit 一起,在同一次 `git push origin main` 送出。若該 repo 的 staging workflow 掛了 `push: branches: [main]` 且帶 `cancel-in-progress`,二次 main push 會觸發新的 staging run 把發版 commit 的 run 取消(見 `~/offline/clade/vendor/snippets/deploy-gate/README.md`);沒有 main-push 觸發的 repo 則單純少一次無謂 push。本步驟收集的內容不依賴版本號或 push 結果,版本號在 Step 6-A 才產生(走 6-B 時不產生版本號)。

### 5-A. 判斷是否需要 handoff

檢查以下任一條件成立 → 需要 handoff:

- `flow status` 仍有未 `done` 的 work item,或 `tasks/` 有本 session 未收尾的 carrier
- `git status` 仍有 uncommitted 變更(刻意未入本次 commit 的 WIP)
- 本次 session 中提及但未做的後續工作(例:refactor 機會、文件更新、測試補強、效能優化)
- 本次 commit 揭露的新 follow-up(TD 引用、TODO 註解、scope 外發現)
- commit 後必要的驗證 / 部署步驟(人工檢查、deploy smoke test、DB migration 套用)
- 使用者曾提過但還沒做的事(在本 session 或前 session 出現過的 backlog)
- 使用者明確表達接下來要交接 / 暫停

全部不成立(真正什麼都沒得做了)→ 跳到 5-D:若 `HANDOFF.md` 存在且內容已過時,清空或刪除。

### 5-B ~ 5-F:收集 → 寫 HANDOFF → 同步 ROADMAP → 納入 commit → 報告

5-A 判定**需要 handoff** → **MUST 讀 [`handoff-steps.md`](handoff-steps.md) § 5-B~5-F 並逐步執行**,五個子步驟一個都不能跳(5-E 的「HANDOFF/ROADMAP 變更 MUST 在此 commit、但不 push」是 Step 6 單次 push 設計的前提,漏掉會讓 working tree dirty 進 deploy commit)。

5-A 判定**不需要** → 跳到 5-D 的清理路徑(同檔)。

## Step 6-Gate: 發版授權判定(**Step 6 的必做第一步**)

Step 6 會建立並推送 release tag。**對 production 由 tag 觸發的 repo,那一步等於 agent 自行決定部署 production**(含跑 production migration)。因此 **MUST** 先判定本 repo 的發版觸發形狀,**NEVER** 直接進 Step 6 的 bump / tag。Unattended coordinator 在 merge **前**就跑同一支 `deploy-trigger-check.ts`;本步驟只處理已 land 之後的發版分流。預先核准 `release=manual` 時停在 6-B,記「已 land、未發版」,不逐張追問 Charles 是否發版。

```bash
node scripts/deploy-trigger-check.ts
```

它讀 `.claude/consumer-meta.json` 的宣告,**同時**從 `.github/workflows/` 推導實際觸發條件,兩邊一致才給放行。判定看 `verdict=` 那一行:

| `verdict=` | 意義 | Step 6 走哪條 |
| --- | --- | --- |
| `confirmed-push-main` | 宣告 `push-main`,且 production deploy workflow 確實由 main push 觸發 —— 不建 tag 也照樣部署,攔 tag 沒有保護作用 | **走 6-A(現行完整流程)** |
| `needs-approval` | 其餘全部:`tag-v` / `manual`、宣告缺漏、宣告與 workflow 不符、workflow 推不出單一結論、腳本不存在 | **走 6-B(不建 tag;push main 本身另判授權)** |

**放行條件是「宣告與 workflow 一致」,不是「宣告寫了 push-main」。** 這個 gate 曾經只讀宣告,而宣告是手寫的:<consumer-b> 宣告 `push-main`、實際 `push: tags: ['*']`,於是拿到 6-A —— 沒有任何一步會講出這件事,2026-08-22 靠人工 grep workflow 才發現。現在推導失敗、宣告與現實矛盾一律 fail-closed 到 6-B,**宣告錯誤換不到寬鬆分支**。

`scripts/deploy-trigger-check.ts` 不存在(consumer 尚未收到該次散播)時 **MUST 走 6-B**,**NEVER** 退回舊的「只讀宣告」查法——那正是這條 gate 要取代的東西。

**NEVER 因為「這個 repo 我記得是 push-main」就跳過這條查詢。**推錯的方向是不可逆的(tag 一推出去 production 就開始跑)。

> `status=` 那一行同時是 finding 來源:`undeclared`(缺 `deploy.deployTrigger`)、`mismatch`(宣告與 workflow 矛盾)、`unconfirmable`(`derived=ambiguous`:deploy workflow 掛了 ≥2 種觸發——分散在不同 workflow 時,宣告要填 **production** 的那個;同一個檔案裡 production job 分不出來時 detail 會寫 `changing the declaration will not help`,改宣告救不了,要拆檔或替 job 加 `environment:` + ref 的 `if:`;或 `derived=none`:根本找不到 deploy workflow——後者不是宣告寫錯,是沒有東西可宣告)。**列進 Step 7 完成報告**,但 **NEVER** 在本次 `/commit` 順手改它——那是獨立的宣告修正,要單獨走。

## Step 6-A: 版本號升級與 Deploy Commit(`push-main` 專用)

判斷升級類型:

- 包含 `feat` → `pnpm version minor --no-git-tag-version`
- 只有 `fix` 或其他 → `pnpm version patch --no-git-tag-version`

建立 deploy commit:

```bash
git commit --only -m "$(cat <<'EOF'
🚀 deploy: 發布新版本 v{新版本號}

- 功能描述一
- 功能描述二

Via: /commit
EOF
)" -- package.json &&      # pnpm-lock.yaml 若一起 bump 就一併列進 pathspec
  git push origin main &&
  git tag "v{新版本號}" &&
  git push origin "v{新版本號}"
```

`--only` 與 `&&` 兩件都不是風格:

- **`git add` + 裸 `git commit` 會把 index 裡別的東西一起收進 deploy commit**——被 gate 刻意擋下不 commit 的檔、別 session 預 stage 的檔都算。那正是 `rules/core/commit.detail.md` § Ad-hoc commit 要求 `--only` 的原因,deploy commit 不是例外。
- **沒有 `&&` 時 commit 失敗照樣往下跑打 tag 那一步,tag 就建在上一個 commit 上**(<consumer-b> v1.275.1,2026-09-03 實測:deploy commit 被 pre-commit gate 擋下 exit 1,tag 仍建出來,救回要刪 tag、`--only` 重 commit、重建 tag)。後果不對稱——commit 失敗是本機的事,tag 建錯是對外的事。

**tag 打在 main push 之後不是順手排的**:`rules/core/commit.detail.md` § Tag 位置 要求打 tag 當下
`git rev-list --count origin/main..HEAD` 必須是 0。先打再推 main 的話,打 tag 的那一刻這個數字
非 0(Step 4 的分組 commit、Step 5 的 HANDOFF/ROADMAP commit、本步驟的 deploy commit 都還沒推)——順序倒過來就同時滿足了那三步,而且競態時本機根本還沒有 tag 要刪。

**不照上面整條 `&&` 串一次貼、而是分步驟手打時,打 tag 之前 MUST 確認 tip 就是這次的 deploy
commit**(串成一條時由 `&&` 保證,不必另外跑):

```bash
git log -1 --format=%s     # MUST 是 🚀 deploy: 發布新版本 v{新版本號}
```

`git tag` 在目前 tip(Step 5 的 HANDOFF/ROADMAP commit 與本步驟的 deploy commit 都已在同一條線上)建立 `v{版本號}` **local** tag。

**這裡 NEVER 用 `pnpm tag`**,即使該 repo 有這支 script。它在多數 consumer 上**自己就會 push**——2026-09-04 實測 `package.json` 的 `scripts.tag`:<consumer-a> / <consumer-d> / <consumer-c> 都是 `git tag v… && git push origin --tags`(只有 <consumer-b> 是純本機 `git tag`)。把它放在樣板裡,tag 會在 main 之前送出去,`tag-position` 當場擋下、`&&` 整條中止,**main 與 tag 兩個都沒上去**——正是 TD-906 那個死鎖。逐字反開脫:「這個 repo 的 `pnpm tag` 我記得只打本機」——那正是要量的東西,而 `git tag` 在四個 repo 上行為相同,量都不必量。

### 推送順序:無條件 main 先、tag 後

推的是**具名 tag**(`git push origin "v<版本>"`),**NEVER `git push origin --tags`**——
`--tags` 推的是本機所有 tag,這次要送出去的是哪一個由本機殘留決定,不由這次發版決定。
(這條約束的是**本步驟**這條發版序列。clade 自家的 `/clade-publish` 是另一套 SOP:`scripts/publish.ts`
會**印出**一條 `git push --tags` 當作下一步指令,順序同樣是 main 先,只是用了 `--tags` 寫法,不受本條管。)

順序**無條件**是 main 先、tag 後,依 `rules/core/commit.detail.md` § Tag 位置——那條規約
無條件要求 push tag 前不得有未推 commit,與這個 repo 接了什麼 pre-push check 無關。
`vendor/scripts/pre-push/checks/tag-position.sh` 是它的機械兜底,tags-first 必定被擋
(<consumer-b> v1.275.1、<consumer-a> v0.111.0 實測)。**NEVER 先量這個 repo 有沒有接 `tag-position`
再決定順序**——沒接的 repo 只是少了兜底,規約本身沒有變。

**被 `tag-position` 以「超前」擋下的 push,NEVER 用 `--no-verify` 或 `CLADE_ALLOW_STALE_TAG`
過關**:tag 指向 origin 上還不存在的樹,是推出去就收不回的對外物件。

它是**政策,不是機械保證**——所以「反正 script 會攔」不成立:`CLADE_ALLOW_STALE_TAG` 在
`tag-position.sh` 裡的判斷排在方向計算**之前**,設了就兩個方向一起放行(2026-09-04 實測:tag
超前 origin/main 一個 commit,未設回 exit 1、設了回「已設,放行」exit 0)。擋住超前方向的只有
上面那條規約本身。

(2026-09-21 TD-911:gate 對同批 `--atomic` 且 `refs/heads/<default>` head **即** tag
commit 的推送放行——放行語義只在原子批次下成立,嚴格祖先仍歸 stale 擋法、同批其他
branch 不算數,細節見 `rules/core/commit.detail.md` § Tag 位置。這是放寬 gate,**不改**
本節的 main 先、tag 後序列。)

(`tag-position` 也擋「落後」方向,那一邊**有**合法用途——刻意在舊 commit 上打 hotfix release
tag——判準見 `rules/core/commit.detail.md` § 機械 gate 與它的邊界,**NEVER** 把本條讀成連那一邊
也一起禁掉。)

**main 先不牴觸 deploy-gate 的「發版窗口內不 push main」**:那條防的是**別的**工作在窗口內 push,取消掉發版 SHA 的 staging run。發版序列自己的那一次 main push 是窗口的**起點**,不是窗口內的干擾。窗口的完整定義見 `~/offline/clade/vendor/snippets/deploy-gate/README.md` § 操作規約,race 與 gate 的 recovery 見同檔的 § 這道 gate 有一個消不掉的 race 與 § Recovery。

**tag-only 那趟 push 會讓 pre-push 全套 8 支照跑**:`vendor/scripts/pre-push/runner.sh`
在只推 tag 時算不出 changed paths,刻意 fail-open 成全跑(該檔 `PATH_FILTER_ACTIVE` 的 `else`
分支印出「算不出 changed paths(新 branch 首推 / 手動執行 / 只推 tag)→ 全部照跑」)。8 支並行,
wall time 由最慢的單一 check 決定(<consumer-b> 2026-09-07 實測暖路徑 `nuxt-typecheck` 2m29s、冷路徑 push
全程 9m22s;該檔註解裡 2026-07-26 的 57.9s 已過期,**NEVER** 直接引用,要用先自己重量)。這是 main-first
的已知成本,**NEVER** 用 `--no-verify` 省它。

**序列中途失敗的復原**(七格都要會)。**先問一句:這個版本號的 tag 已經推出去了嗎**
(`git ls-remote --tags origin "v<版本>"`,有輸出就是已公開)——沒公開的 tag 怎麼刪重打都是本機
的事,已公開的 tag 是對外物件,**NEVER** 拿下面任何一格當作刪遠端 tag 的授權(唯一的例外是本節
最後那段 2026-06-03 的「tag 推出去了但 workflow 沒觸發」,那裡刪並重推的是**同一個 SHA 上的同名
tag**,內容不變)。

**下面每一格都適用的一條**:`pnpm version` 跑在 `&&` 串**之前**,所以進到任何一格時版本號都已經
bump 過了——**NEVER 因為「重跑一次比較乾淨」而再 bump 一次**。每一格都是**從失敗的那一步接著做**,
只有明寫「改用下一個版本號」的那幾格才重新 bump。

- **`git commit --only` 這一步就失敗**(典型:pre-commit gate 擋下,<consumer-b> v1.275.1 實測)→ 版本號
  **還在 working tree 上**(見上面那條共通規則)。照 gate 的訊息修好之後,只重跑
  `git commit --only … && git push origin main && git tag … && git push origin …` 這一串。**修正若動到 `package.json` 以外的檔**,
  那個 `--only … -- package.json` 的 pathspec 會把它們留在 working tree(下一趟 push 多半再被同一支
  gate 擋下)——**MUST** 決定它們要不要一起進 deploy commit,要就把路徑加進 pathspec,不要就先另外
  commit 掉。**若 `git commit --only` 的失敗訊息是「nothing to commit」或「no changes added to commit」**(後者出現在 `package.json` 已 commit、但別的檔還 dirty 時),代表上一趟其實已經 commit
  成功了(版本號已在 HEAD、不在 working tree)——這一格的前提不成立,**MUST** 跳到下一格從
  `git push origin main` 接著做。
- **`git push origin main` 這一步失敗**(競態/權限/網路/被某支 pre-push check 擋下)→ 此時
  tag 還沒打、什麼都還沒公開,沒有對外的東西要救。但 deploy commit **已經建好了**——從頭重跑本步驟
  會再建一個 `🚀 deploy:` commit 並跳掉一個版本號,正好製造 Step 6-B 警告的「main 上有發布 vX 卻沒有
  tag」。**MUST 從失敗的那一步接著做**:照錯誤訊息修好之後跑 `git push origin main && git tag "v{新版本號}" && git push origin "v{新版本號}"`。
  **這一串沒有 commit 那一節,所以上面那條「tip 必須是這次的 deploy commit」不再由 `&&` 保證——
  接之前 MUST 自己跑一次 `git log -1 --format=%s`**。修 pre-push check 通常會多出新的 commit,
  那時 tip 已經不是 deploy commit 了:要嘛把修正 squash 進 deploy commit,要嘛接受 tag 打在新的
  tip(deploy commit 訊息仍寫著同一個版本號,內容多了那次修正——可以,但 MUST 是你**知道**的選擇)。
  競態(remote 有新 commit)**MUST** `git pull --rebase` 後再接這一串,**NEVER** 用 `--force`
  (會把別人的 commit 從 remote 抹掉)。
- **main 已上、推 tag 失敗(權限/網路)** → 主線沒有分叉,**直接重推 tag** 即可,**NEVER** 為了
  「趕快觸發部署」改回 tags-first。**NEVER 把這一格讀成「部署還沒開始,慢慢來」**:走到 6-A 的
  一般路徑是 `confirmed-push-main`,那個形狀下**剛才那次 main push 就是部署**(見 Step 6-Gate 的
  判定表)——tag 在這裡是紀錄,不是觸發器。重推之前若 remote main 又前進了,會落到下面「被
  `tag-position` 以落後擋下」那一格。
- **`git tag` 這一步就失敗**(exit 128:同名 tag 本機已存在,多半是上一趟沒收乾淨)→ 此時 main
  已上、tag 沒建,**NEVER** 直接「重推 tag」(那會把本機那個指向舊 commit 的同名 tag 推出去)。
  先 `git ls-remote --tags origin "v<版本>"` 問 remote——**`git show-ref` 只看得到本機,答不了這一題**。
  remote 沒有 → 是廢棄的本機殘留,`git tag -d` 後重打;remote 已經有 → 這個版本號已經發過,
  **MUST** 改用下一個版本號(`pnpm version patch --no-git-tag-version` 重跑本步驟——**NEVER 漏掉那個旗標**,裸 `pnpm version` 會順手建 commit 與 tag,正好把你送回這一格),**NEVER** 刪遠端 tag 去讓路。
  main 上那個沒有 tag 的 `🚀 deploy: … v<舊版本>` commit **MUST** 照下面最後一格的做法留一行說明。
- **main 已上、推 tag 被 `tag-position` 以外的 pre-push check 擋下** → tag-only 那趟會把 8 支全跑
  (見上段),所以擋你的可能是 lint / typecheck / ratchet 任何一支。**MUST 照那支自己的訊息修好
  再推 tag**,**NEVER** 用 `--no-verify` 繞過(main 已經公開,這時候放行等於讓 tag 指向一棵沒
  過 gate 的樹)。**修正若產生了新的 commit(修 lint / typecheck 幾乎必然如此),MUST 先
  `git push origin main` 把它們推上去,再推 tag**——tag 要指向的就是修完之後的那棵樹。
  推完 main 之後若 remote main 又被別人前進了,才落到下面「被 `tag-position` 以落後擋下」那一格;
  **NEVER 帶著本機未推的 commit 直接走進那一格**,它的 `git merge --ff-only origin/main` 在那個
  狀態下必定回 `Not possible to fast-forward`。
- **main 已上、`git tag` 也成功,但推 tag 被 remote 以 `! [rejected] … (already exists)` 拒絕**
  → 這個版本號的 tag **已經在 remote 上**(`tag-position` 只看本機與 `origin/<default branch>`
  的關係,不看 remote 有沒有同名 tag,所以它會放行)。成因典型是先前某趟 `--tags` 殘留、或
  另一個 clone/worktree 已經推過。**NEVER** 反覆重推(會一直被同一個理由拒絕),**NEVER**
  刪遠端 tag 讓路——**MUST** 改用下一個版本號(同樣帶 `--no-git-tag-version`)重跑本步驟,並照下面最後一格的做法替 main 上那個
  沒有 tag 的 `🚀 deploy: … v<舊版本>` commit 留一行說明。
- **main 已上、但推 tag 被 `tag-position` 以「落後」擋下** → 代表**別的 session 在你推 main 與推
  tag 之間又 push 了 main**(那本身違反 deploy-gate 的「發版窗口內不 push main」——窗口從**發版序列自己的那一次 main push** 起算,見上面那段與 `vendor/snippets/deploy-gate/README.md` § 操作規約)。
  正解是**在新的 HEAD 上重跑一次發版序列**:`git tag -d "v<舊版本>"`(本機那個沒公開的,刪掉)
  → `git fetch origin main && git merge --ff-only origin/main`(**先併進來,否則下一步 push 必被
  non-fast-forward 拒絕**)→ `pnpm version patch --no-git-tag-version` → 新的 deploy commit →
  push main → 打**新版本號**的 tag → 推 tag;那些插進來的 commit 就一起出這一版。
  main 上會留下一個沒有對應 tag 的 `🚀 deploy: 發布新版本 v<舊版本>` commit——**MUST** 在新的
  deploy commit 訊息或 HANDOFF 裡寫一行「v<舊版本> 未發版,內容併入 v<新版本>」,否則下一個
  接手的人會照 Step 6-B 的理由誤判已發版。
  **NEVER** `git tag -d` 之後把**同一個版本號**重打在別人的 commit 上(版本號與內容從此對不起來),
  **NEVER** 用 `CLADE_ALLOW_STALE_TAG` 過關——那個逃生口只留給「刻意在舊 commit 上打 hotfix
  release tag」,把它用在這裡就是拿它繞過一個你其實看得懂的攔阻。

> 2026-06-03 v1.185.1 實證:「main 先、tag 後」分兩步推送**同一個 SHA** 時,GitHub 先收到 main
> commit SHA、再收到指向同一 SHA 的 tag,有機率不觸發 `push:tags` workflow。修法是刪掉並重推
> 同名 tag(`git push origin :refs/tags/v<版本>` → `git tag -d "v<版本>"` → `git tag "v<版本>"` →
> `git push origin "v<版本>"`)。
>
> 因此**只要這次推的 tag 會觸發任何 workflow**,就 MUST 在推 tag 之後實際確認它跑起來了。
> 純 `push-main` 形狀(Step 6-Gate 回 `confirmed-push-main`,也就是走到本步驟的一般路徑)沒有
> tag-triggered run,這一格標「不適用」即可;真正會命中的是 **6-B 選 `[1]` 後回頭執行本步驟**
> 的形狀,以及 6-Gate 的 `derived=ambiguous`(deploy workflow 掛了 ≥2 種觸發——同一支同時吃 main push 與 tag push,或兩支各吃一種)。
> **NEVER** 因為「本步驟叫 `push-main` 專用」就把這段當成永遠不必做——判「這次的 tag 觸不觸發」
> 看的是 6-Gate 印出來的 **`derived=`**,不是本步驟的標題,也**不是 `status=`**:
> `status=unconfirmable` 同時涵蓋兩種完全相反的形狀——`derived=ambiguous`(deploy workflow 掛了
> ≥2 種觸發,**tag 會觸發**,這一格要確認)與 `derived=none`(**根本沒有 deploy workflow**,tag
> 什麼都不會觸發,這一格「不適用」)。把兩者當成同一件事,在 `none` 那格會讓你對一個**已公開**的
> tag 執行刪除重推——本節唯一不可逆的動作——去追一條本來就不存在的 run。
> **判定用「哪些要做」而不是「哪些不做」**——`derived=` 只有六個值,其中**只有 `tag-v` 與
> `ambiguous` 代表這次的 tag 真的會觸發**,要走下面的確認:
>
> | `derived=` | tag 會觸發嗎 | 這一段 |
> | --- | --- | --- |
> | `tag-v` / `ambiguous` | 會 | **MUST 確認** |
> | `push-main` / `pr-merge` / `manual` / `none` | 不會 | **NEVER 進入確認與重推**,Step 7 標「不適用(`derived=<值>`)」 |
>
> **NEVER 反過來背成一張「不適用」清單**:那張清單漏一個值,漏掉的那格就會把你送去對一個
> **已公開**的 tag 執行刪除重推,去追一條本來就不存在的 run。`push-main` / `pr-merge` / `manual`
> 都會經由 6-B 的 `[1]` 回頭跑本步驟,不是只有 `confirmed-push-main` 才走得到這裡。確認時 **MUST 用 tag 名過濾**——`-w <production workflow>` 之後,同一個 SHA 上仍可能混進 main push 觸發的**同一支** workflow 的 run:
>
> ```bash
> for i in $(seq 7); do
>   gh run list --workflow <production workflow> --limit 10 \
>     --json headBranch,event,status,createdAt \
>     --jq '[.[] | select(.headBranch == "v<版本>")]' | tee /dev/stderr | grep -q headBranch && break
>   if [ "$i" -lt 7 ]; then sleep 10; fi
> done
> ```
>
> **同一個 workflow 同時由 main push 與 tag push 觸發時(6-Gate 的 `unconfirmable` 形狀),
> NEVER 用 `gh-ci-watch.sh --tag` 代替上面這段**:那個旗標把 tag 解析成 SHA 再用 `-c <SHA>` 過濾
> (`TAG_SHA=$(git rev-parse …)`),而 main-first 之下兩條 run **本來就在同一個 SHA 上**——它分不開
> 兩者,於是「tag 沒觸發」會被 main 那條 run 報成綠。(純 `tag-v` 形狀下 main push 不會產生這個
> workflow 的 run,`--tag` 是對的——那正是 gh-ci-watch skill 逐字要求用 `--tag` 的情境。)
>
> 也 **NEVER 換成 `gh-ci-watch.sh --branch "v<版本>" --timeout 60`** 當「等價寫法」:那支要輪到
> **終態**才回,production run 幾乎不可能在 60 秒內跑完,於是正常情況也回 `WATCH_TIMEOUT` exit 3;
> 它的 `--branch` 模式還自帶 `SINCE = now-120s` 的時間窗與 ≥30s 的輪詢間隔。這裡要問的只是
> 「run 建立了沒」,用上面那圈就好;要看部署**跑完**才用 gh-ci-watch。
>
> **判「未觸發」之前 MUST 先確認 `gh` 本身是好的**(`gh auth status` 或看上面那圈有沒有印出錯誤):
> auth/網路壞掉時 stdout 一樣是空的,而空與「沒觸發」在這裡長得完全一樣——照著往下走就是對一個
> **已經公開**的 tag 做刪除重推,正好是本節唯一被特別授權、也唯一不可逆的動作。
>
> **`gh` 正常、且輪詢滿 60 秒(t=0 起每 10 秒一次,共 7 次)仍查不到該 tag 的 run,才判未觸發**,再走上面的刪並重推,**NEVER** 改回 tags-first
> 去繞過 `tag-position`。

## Step 6-B: 停在 push main,發版另問(`tag-v` / `manual` / `unknown`)

這些形狀下 **建 tag 就是部署 production**(`tag-v`),或部署本來就不由 `/commit` 負責(`manual`)。因此本步驟不建 tag、不發版。

**本步驟 NEVER 做以下四件事**,即使本次 commit 含 `feat`、即使使用者說「commit 完就好」:

- ❌ `pnpm version <patch|minor>` —— 未發版就不該有版本號 bump
- ❌ `🚀 deploy:` commit —— main 上出現「發布新版本 vX」卻沒有對應 tag,會讓下一個接手的人誤判已發版
- ❌ `pnpm tag` / `git tag`
- ❌ `git push origin --tags` / `git push origin v<版本>`(推 tag 就是發版,不論用哪種寫法)

### 6-B.0: 先判 `git push origin main` 本身是不是部署(**先於 push**)

`needs-approval` 是個混合袋。`tag-v` / `manual` 這兩種**已確認**的形狀下,push main 什麼都不會觸發;但「宣告缺漏 / 宣告與 workflow 不符 / 推不出單一結論 / 腳本不存在」這幾格,**沒有人知道 main push 會不會部署 production**。同一句 `git push origin main` 在後者就是一次未經授權的部署 —— 而 6-Gate 攔下 tag 的保護在這裡完全沒有覆蓋到。

判定只看 Step 6-Gate 已經印出來的那兩行,不必再跑任何東西:

| Step 6-Gate 輸出 | 本步驟動作 |
| --- | --- |
| `status=confirmed` **且** `derived=` 不是 `push-main` | 直接 `git push origin main` |
| 其餘全部:`status=mismatch` / `undeclared` / `unconfirmable`,或 `derived=push-main`,或 `deploy-trigger-check.ts` 不存在 | **NEVER 先 push**,先照下面取得授權 |

第二個條件(`derived` 不是 `push-main`)不能省:`declared=pr-merge` + `derived=push-main` 會拿到 `status=confirmed` 卻仍被送進 6-B,而那種拓樸下 main push 就是部署。

未確認時 **MUST** 先透過本入口的使用者詢問介面取得下列選擇,**NEVER** 先推了再問。沒有專用工具時直接提問;同一範圍已有明確回答時沿用,不重問:

- **`[1] 授權 push main`** → 明確告知「本 repo 推不出 main push 會不會觸發部署」後才 `git push origin main`,接著往下走發版提問
- **`[2] 先不 push`** → 停在 local commit。**MUST** 在 Step 5 的 HANDOFF 登記「已 commit 未 push」:哪幾個 commit、卡在哪一格(`status=` / `detail=` 原文照抄)、下一步要補的是宣告還是 workflow

**NEVER 把「使用者沒有回應」讀成 `[1]`。** 這條沒有安全預設值 —— 未確認形狀的預設值是「可能對 production 跑 migration」。

### 6-B.1: push 與發版提問

```bash
git push origin main
```

`git push origin main` 完成後(6-B.0 判定可直接推,或使用者選了 `[1]`),發版尚未取得明確授權時 **MUST** 透過本入口的使用者詢問介面取得下列選擇;沒有專用工具時直接提問。既有同範圍發版授權持續有效:

- **`[1] 現在發版`** → **MUST 先讀該 repo 的 `HANDOFF.md` 發版段與 `.github/workflows/`**,確認有沒有固定發版路徑(例:先跑 precheck workflow 拿到 `SAFE` 才授權 deploy、staging-gate 要求同 SHA 的 staging 已綠)。**有固定路徑就照它走,NEVER 直接 `git push origin --tags` 蓋過去**;沒有固定路徑才回頭執行 6-A 的 bump / tag / push main 與 tag
- **`[2] 先不發版`** → 本次到此為止。**MUST** 在 Step 5 的 HANDOFF 裡登記「已 land 未發版」:寫明哪幾個 commit、下次發版該升 major/minor/patch、以及不發版的理由(若使用者有給)

**NEVER 把「使用者沒有回應」讀成 `[1]`。** 這條問題沒有安全的預設值——`tag-v` 的預設值是「對 production 跑 migration」。

> **Step 6b 的前提**:Step 6b 的 Notion 同步依賴 tag 已推出。走 6-B 且使用者選 `[2]` 時 **MUST 跳過 Step 6b**(沒有 tag 可同步);選 `[1]` 並實際完成發版後才執行。

## Step 6b: Notion hub 同步(條件觸發)

per [[notion-work-coupling]] § 生命週期。consumer 的 `.claude/consumer-meta.json` 若有 `notion.hub`,Step 6-A(或 6-B 選 `[1]` 後)的 tag 已推出後 **MUST** 對本次發版含的每個 work item 執行:

```bash
node ~/offline/clade/vendor/scripts/notion-sync.ts release \
  --consumer-path . --work <work-id> --tag "$(git describe --tags --abbrev=0)" \
  [--prod-url <prod 上看得到修正的頁面>] [--screenshot <驗收畫面.png>]… --json
```

本步驟把連結的 ticket 推到 acceptance(`驗收中`)並填版本、`上線日期`、`PR` 欄(script 從 work item 的 commit/url artifact 查),`備註` 寫 `--prod-url`、內文附「驗收畫面(<tag>)」截圖,把客戶時程頁的 `交付項目` 進度% 寫 100。

- **客戶面證據(D2)**:`--prod-url` 只收 consumer prod 網域(`.claude/consumer-meta.json` `deploy.prodUrl`);GitHub PR/CI/tag 連結 **NEVER** 當證據給客戶——script 會拒寫。有 UI 變更且本次有拍驗收截圖(`[verify:ui]`/screenshot review)就帶 `--screenshot`。
- **綁不到要點名**:本次發版含的 commit 若有對不到任何 work item 的、或 work item 沒有連結 ticket 的,**MUST** 在 Step 7 報告逐條點名(commit/work id +「無 ticket」或「無 work item」),**NEVER** 靜默跳過。客戶提的問題卻沒有 ticket → 回 `notion-hub` 的工程師建票意圖補建。
- `needsDecision` 非空(客戶側狀態、status regression、hub 對映不到現況)→ 尚未取得該動作授權時,**MUST** 透過本入口的使用者詢問介面確認,帶答案重跑;**NEVER** 自動執行任一條。
- ticket `驗收中 → 完成` 是客戶側轉移,script 一律拒絕不自動寫。
- `pending` 非空 → 寫入未確認落地,**MUST** 列進 Step 7 完成報告。
- consumer 未宣告 `notion.hub` → script 自行 exit 0,不需另外判斷。

## Step 7: 完成報告

**批次先收尾**:正式 landed 的批次 MUST 依 [batch.md](batch.md) §4 呼叫 cleanup,再報各來源 removed/retained 原因;PR 未合併保留。清理失敗可獨立重試,不重跑 Step 0。只有來源保存的 checkpoint 或尚在 index 時,**NEVER** 宣稱 main 落地或刪來源。

本步先組裝報告;Step 8 的已授權後續動作與 Final Step 的 writer 收回/鎖釋放完成後,才輸出完整完成宣告。鎖尚未釋放或背景 writer 終態未確認時,報告保留具名未完成項。

**Completion evidence gate**(輸出「✅ Commit 完成」前 MUST 逐格自查;每格附「實跑命令+輸出摘尾」,貼不出證據=該格未完成,不准宣告完成):

- [ ] 每個 commit scope 驗證:貼各 commit 的 `git show --stat <hash> | tail -3` 輸出(changed files 數 vs 預期不符 → 回 Step 4 處置,不出報告)
- [ ] Step 0 品質檢查結論:貼 0-A / 0-C 的結論行(條件未觸發的軸標明「未觸發+原因」)
- [ ] 本次其他已觸發 gate:依 runtime-gates.md 附對應結果,不以 0-A/0-C 通過代替 UI、security、schema、文件或 coverage 的完成證據
- [ ] Step 6-Gate 判定:貼 `deploy-trigger-check.ts` 的實際輸出(含 `verdict=` / `status=` / `detail=`),並標明走 6-A 還是 6-B(`status=` 不是 `confirmed` 時 MUST 一併把 `detail=` 列成 finding)
- [ ] push / tag 結果:走 6-A 貼 `git push origin main` 與 `git push origin v<版本>` 兩者輸出(順序即實際執行順序),`derived=` 是 `tag-v` / `ambiguous` 時另貼 `gh run list` 顯示該 tag 的 run 已排入(其餘四個 `derived=` 值都沒有 tag-triggered run,標明「不適用(`derived=<值>`)」即可);走 6-B 貼 `git push origin main` 輸出 + 使用者對發版問題的選擇(本次不 push 則標明原因)
- [ ] 退出:Final Step 的 writer 終態與自己的 release receipt;需要繼任者接手時,列 owner、未完成 handle 與接續條件,不宣稱已安全退出

以上證據放進完成報告的 `Evidence` 段——**照下面樣板的行逐行對應,不是固定三格**(checklist 的最後一格在樣板裡拆成 `push:` 與 `deploy-trigger:` 兩行)。

走 6-B 且使用者選「先不發版」時,完成報告 **MUST** 把「版本 / Tag」兩行改成 `未發版(verdict=needs-approval / status=<值>,已 push main,發版待人拍板)`,**NEVER** 沿用下面樣板的「已建立並推送」字樣。

```text
✅ Commit 完成!

共建立 N 個 commit:
1. abc1234 feat: ...
2. def5678 fix: ...
3. jkl3456 docs(handoff): 更新 commit 後交接狀態
4. ghi9012 deploy: 發布新版本 v1.8.0

版本:1.7.1 → 1.8.0 (minor)
Tag:v1.8.0 已建立並推送

Evidence:
- scope: <各 git show --stat 摘尾>
- checks: <0-A/0-C 結論行或未觸發原因>
- release-gate: verdict=<實際輸出> status=<實際輸出> → 走 Step 6-<A|B>
- push: <git push origin main 與 git push origin v<版本> 摘尾>
- deploy-trigger: <`derived=` 是 tag-v / ambiguous 才貼 gh run list 過濾該 tag 的 run 摘尾;其餘 derived= 值與走 6-B 未發版時寫「不適用(derived=<值>)」>
```

## Step 8: 自動銜接 /ship(條件觸發)

```bash
git branch --show-current
```

**不在 main / master 分支** 且本 runtime 實際提供 `ship` skill → **MUST 讀 [`handoff-steps.md`](handoff-steps.md) § Step 8** 依已有授權或詢問結果銜接。批次 trunk 已落地時在 main 接續。批次 **ready** 若該 `workId` 已有 draft PR,MUST 把受審 formal HEAD 交到**那一張** PR 的 head ref,再標 ready;**NEVER** 另開第二張。來源 draft 本身不構成 ready。Unattended landing 沿用同一張 PR,**NEVER** 在 Step 8 重開 PR。在 main / master、或本入口未提供 `ship` → 不觸發,直接進 Final Step。

## Final Step: 釋放 /commit lock(**必做最後一步**)

**MUST 依 [runtime-lifecycle.md](runtime-lifecycle.md)「背景工作與退出」收回本 ceremony 的工作,再用原 work/runtime/session 與 owner token 執行 release 並保留 receipt。** 正常完成、gate 失敗與使用者中止都走同一退出路徑。

若仍有會改檔/index/ref 的工作且未確認停止,或鎖的 owner 已改變,保留鎖及對應 handle/失敗證據,回報接手責任;不以 timeout 自動清鎖或宣稱已釋放。


# Runtime adapter: Cursor

Cursor 主線在**當前 checkout**(含 linked worktree)跑完整 commit ceremony:lock、WIP/ownership、品質閘門、分組,以及 gates 通過後的 `git commit --only`。這不是把整場 `/commit` 轉派給 Herdr Claude。

- `COMMIT_RUNTIME` 為 `cursor`;`COMMIT_SESSION_ID` 取本 session 已暴露的 `CURSOR_SESSION_ID`(或缺席時的同等原生 session id)。缺身分就停在 Step 0-Lock。
- 讀檔、diff、shell、對話詢問使用本入口 catalog。不要把 `--cwd` 改到 main worktree 假裝 WIP 在那裡。
- 0-A.1/0-A.2 唯一合格 reviewer 是 fresh-context Claude Opus 5.5 medium,走 `claude-review-safe.sh medium`。Opus 不可用時 gate 保持未完成;Cursor Task/Agent 不充 reviewer,也不以其他模型補位。
- commit 流程主線不需要自己開 Herdr pane(0-A 的 Opus reviewer 由 `claude-review-safe.sh` 內部以 Herdr create-only 承載);**NEVER** `--relay`,也 NEVER 把後續 `git commit` 丟給任何 pane。
- 0-B 使用本 session 已暴露的 browser/截圖能力與合格視覺 reviewer。
- 缺合格載體的 gate 保持未完成;不因此改派整場 ceremony 到 Claude,也不跳過品質要求逕行 `git commit`。

Files in this skill

  • SKILL.md49.5 KB
  • gates.md88.4 KB
  • handoff-steps.md4.5 KB
  • schema-sync.md6.4 KB
  • security-scan.md7.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.