From 221742486bed5a089f647249e36106332d807e1a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 26 Aug 2026 17:59:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(sdlc-gate):=20=E5=B7=A5=E4=BD=9C=E5=8C=85?= =?UTF-8?q?=E9=8E=96=E6=AA=94=E6=94=B9=E4=BE=9D=E7=B4=A2=E5=BC=95=E5=8D=80?= =?UTF-8?q?=E5=88=86=EF=BC=8C=E9=81=BF=E5=85=8D=E5=B9=B3=E8=A1=8C=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=85=E4=BA=92=E7=9B=B8=E8=A6=86=E8=93=8B=E4=B8=A6?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=8F=90=E7=A4=BA=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- hooks/sdlc-gate.sh | 36 +++++++++++++++++++++++++----------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bf02786..471900a 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 | `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖。子指令:`start` 記起始時間(已有紀錄就不動,給 claude 這種每階段有自己 session id 的 CLI)、`restart` 一律覆寫起始時間(給接不到 session id 的 kiro,不覆寫會把上一階段算進來)、`mark` 更新最後活動時間、`report` 供 `jsc-log:worklog` 取花費時間 | | `hooks/version-guard.sh` | PreToolUse(Skill) | 技能使用前的版本前置檢查:本機**實際載入**版本落後遠端發佈版本就以 exit 2 擋下該次呼叫並提示更新指令(更新指令依當前 CLI 給)。只擋落後這一種情況:超前放行(開發技能組時本機本來就會超前),讀不到本機版本、推導不出站台、查不到遠端版本也一律放行。逃生門 `JSC_VERSION_GUARD=off`。豁免 `jsc-cli:deploy`、`jsc-hooks:hooks-install`、`jsc-cli:models`、`jsc-meta:*` | | `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 | -| `hooks/sdlc-gate.sh` | UserPromptSubmit、PreToolUse(Skill) | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從 transcript 讀出實際模型 id 比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖;`check` 在模型不符時以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`unlock` 為逃生門。另含工作包 PR 閘門:`wp-lock {owner}/{repo} {index}` 記下一筆未結清的工作包 PR、`wp-unlock {owner}/{repo}` 結清(檔案不存在也算成功)、`wp-report` 印出所有未結清、`wp-check {prompt|skill}` 為 hook 模式。狀態檔在 `$JSC_HOME/wp/{owner}-{repo}.pr`,**刻意不綁 session**——PR 沒合併時換一個工作階段照樣要擋。`wp-check prompt` 只注入提醒、絕不擋提示(擋了連「去修那支 PR」的對話都送不出去);`wp-check skill` 在有未結清 PR 時以 exit 2 擋下 `plan`、`analyze`、`maintain`,但一律放行 `implement`(結清 PR 正是 implement 的步驟,擋它會鎖死流程)。逃生門 `JSC_WP_GATE=off`。這道閘門只讀檔案、不打網路,PR 的真實合併狀態由 `jsc-sdlc/tools/wp-gate.sh` 查證 | +| `hooks/sdlc-gate.sh` | UserPromptSubmit、PreToolUse(Skill) | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從 transcript 讀出實際模型 id 比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖;`check` 在模型不符時以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`unlock` 為逃生門。另含工作包 PR 閘門:`wp-lock {owner}/{repo} {index}` 記下一筆未結清的工作包 PR、`wp-unlock {owner}/{repo} {index}` 結清那一筆(檔案不存在也算成功)、`wp-report` 印出所有未結清、`wp-check {prompt|skill}` 為 hook 模式。狀態檔一個工作包一支,在 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr`,**刻意不綁 session**——PR 沒合併時換一個工作階段照樣要擋;一個工作包一支鎖檔是為了讓好幾個互不相依的工作包能同時記在案,不會互相覆蓋掉對方的鎖。`wp-check prompt` 只注入提醒、絕不擋提示(擋了連「去修那支 PR」的對話都送不出去);`wp-check skill` 在有未結清 PR 時以 exit 2 擋下 `plan`、`analyze`、`maintain`,但一律放行 `implement`(結清 PR 正是 implement 的步驟,擋它會鎖死流程)——這一層是整個存取庫共用的粗粒度提醒,「某個候選工作包能不能挑」的細粒度判斷在 `jsc-sdlc/tools/wp-gate.sh check-deps`,不是這裡。逃生門 `JSC_WP_GATE=off`。這道閘門只讀檔案、不打網路,PR 的真實合併狀態由 `jsc-sdlc/tools/wp-gate.sh` 查證 | Claude 由 `hooks/hooks.json` 自動接線五支 hook;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。 diff --git a/hooks/sdlc-gate.sh b/hooks/sdlc-gate.sh index fe4202b..cafc507 100755 --- a/hooks/sdlc-gate.sh +++ b/hooks/sdlc-gate.sh @@ -19,13 +19,21 @@ # # sdlc-gate.sh wp-lock {owner}/{repo} {index} 記下一筆未結清的工作包 PR。 # exit 0 = 已記下;exit 2 = 用法錯誤或寫不進狀態檔(沒記下等於沒鎖)。 -# sdlc-gate.sh wp-unlock {owner}/{repo} 結清後移除狀態檔;檔案不存在也算成功。 +# sdlc-gate.sh wp-unlock {owner}/{repo} {index} 結清後移除該工作包的狀態檔;檔案不存在也算成功。 # exit 0 = 已結清;exit 2 = 用法錯誤。 -# sdlc-gate.sh wp-report 印出 {owner}/{repo} {index} {上鎖時間};沒有未結清就不印,exit 0。 +# sdlc-gate.sh wp-report 印出 {owner}/{repo} {index} {上鎖時間},每個未結清工作包各一行; +# 沒有未結清就不印,exit 0。 # sdlc-gate.sh wp-check prompt hook 模式(UserPromptSubmit):注入提醒,一律 exit 0。 # sdlc-gate.sh wp-check skill hook 模式(PreToolUse,matcher Skill):命中別的階段技能時 # exit 2 擋下該次呼叫;其餘 exit 0。 # +# 鎖檔一個工作包一支($JSC_HOME/wp/{owner}-{repo}-{index}.pr),不是整個存取庫共用一支: +# SDLC 實作可能同時有好幾個互不相依的工作包平行進行,各自開各自的 PR。整庫共用一支鎖檔 +# 只留得住「最後一個 lock 的那一包」,先前還沒合併的那幾包會被覆蓋掉,鎖跟著憑空消失。 +# 這支鎖檔管的是「plan/analyze/maintain 能不能在這個存取庫上動」(見下方 wp-check skill), +# 跟「implement 挑下一個工作包能不能挑到某一包」是兩件事——後者的判斷依據是該包在分析頁 +# WBS 表的相依欄,走 jsc-sdlc/tools/wp-gate.sh check-deps,不靠這支鎖檔。 +# # exit code 例外:其他 jsc hook 一律 exit 0 不中斷宿主 CLI;本檔 check 與 wp-check skill 是 # 刻意的例外——鎖存在且不合規時 exit 2 擋下。只用提示注入的話模型可以無視,閘門形同虛設。 # 無鎖、或資料不足無法判定時,仍照舊 exit 0 安靜降級。 @@ -107,7 +115,7 @@ current_model() { printf '%s' "$m" } -# --- 工作包 PR 閘門(狀態檔:$JSC_HOME/wp/{owner}-{repo}.pr) --- +# --- 工作包 PR 閘門(狀態檔:$JSC_HOME/wp/{owner}-{repo}-{index}.pr,一個工作包一支) --- # # 刻意不綁 session:PR 沒合併就是沒合併,換一個工作階段照樣要擋。綁 session 等於給閘門 # 留一道「開新對話就自動繞過」的門,規則就不再是強制的。 @@ -117,8 +125,8 @@ current_model() { WP_DIR="$JSC_HOME/wp" -wp_state_file() { # $1={owner}/{repo} - printf '%s/%s.pr' "$WP_DIR" "$(printf '%s' "$1" | tr '/' '-')" +wp_state_file() { # $1={owner}/{repo} $2=index + printf '%s/%s-%s.pr' "$WP_DIR" "$(printf '%s' "$1" | tr '/' '-')" "$2" } # 未結清清單,每行「{owner}/{repo} {index} {上鎖時間}」;沒有就不輸出。 @@ -232,18 +240,22 @@ case "${1:-check}" in echo "[jsc][工作包閘門][ERR]:PR 編號須為數字,收到「${idx:-空值}」。" >&2; exit 2 ;; esac mkdir -p "$WP_DIR" 2>/dev/null || true - wpf=$(wp_state_file "$repo") + wpf=$(wp_state_file "$repo" "$idx") printf '%s\t%s\t%s\n' "$repo" "$idx" "$(now_iso)" > "$wpf" 2>/dev/null || { echo "[jsc][工作包閘門][ERR]:寫不進狀態檔 $wpf,工作包鎖未生效。" >&2; exit 2; } - echo "[jsc][工作包閘門][OK]:已記下 $repo 第 $idx 號 PR 未結清,結清前不得開新工作包。" + echo "[jsc][工作包閘門][OK]:已記下 $repo 第 $idx 號 PR 未結清。相依於它的工作包在它結清前不得開始;其餘互不相依的工作包不受影響。" exit 0 ;; wp-unlock) - repo="${2:-}" + repo="${2:-}"; idx="${3:-}" wp_valid_repo "$repo" || { echo "[jsc][工作包閘門][ERR]:存取庫須為 {owner}/{repo} 格式,收到「${repo:-空值}」。" >&2; exit 2; } + case "$idx" in + ''|*[!0-9]*) + echo "[jsc][工作包閘門][ERR]:PR 編號須為數字,收到「${idx:-空值}」。" >&2; exit 2 ;; + esac # 冪等:狀態檔不存在也算成功。結清流程可能被重跑,第二次失敗只會讓呼叫端誤判。 - rm -f "$(wp_state_file "$repo")" 2>/dev/null || true + rm -f "$(wp_state_file "$repo" "$idx")" 2>/dev/null || true exit 0 ;; wp-report) @@ -259,7 +271,9 @@ case "${1:-check}" in prompt) # 只注入提醒,一律 exit 0。擋提示會連「去把那支 PR 修好」的對話都送不出去, # 把使用者鎖在門外,連逃生門都下不了指令。 - echo "[jsc] ${brief} PR 尚未合併,禁止開新工作包;請先把該 PR 結清(合併或關閉)再繼續。" + # 這裡只列得出「哪些工作包還沒結清」,不知道候選包相依於誰——沒有上下文可以判斷。 + # 真正「這一包能不能挑」的判斷在 jsc-sdlc/tools/wp-gate.sh check-deps,這則只是提醒。 + echo "[jsc] ${brief} PR 尚未合併。相依於它的工作包不能開始,其餘互不相依的工作包不受影響——是否可挑,見 wp-gate.sh check-deps。" exit 0 ;; skill) # 技能名取法比照 version-guard.sh:環境變數優先,非 Claude CLI 只餵得到環境變數。 @@ -270,7 +284,7 @@ case "${1:-check}" in sname=${skill##*:} case "$sname" in plan|analyze|maintain) - echo "[jsc][工作包閘門][ERR]:${brief} PR 尚未合併,禁止開新工作包,「${sname}」不得進行。請先把該 PR 結清(合併或關閉),或執行 jsc-hooks/hooks/sdlc-gate.sh wp-unlock {owner}/{repo} 解除;確定要整體放行請設 JSC_WP_GATE=off。本次技能呼叫已擋下。" >&2 + echo "[jsc][工作包閘門][ERR]:${brief} PR 尚未合併,禁止在此存取庫執行「${sname}」。請先把該 PR 結清(合併或關閉),或執行 jsc-hooks/hooks/sdlc-gate.sh wp-unlock {owner}/{repo} {index} 解除;確定要整體放行請設 JSC_WP_GATE=off。本次技能呼叫已擋下。" >&2 exit 2 ;; esac # implement 與其餘技能一律放行:結清 PR 正是 implement 步驟 4 要做的事, -- 2.53.0