From 870f52fbaa1e45df1f0ec219c61343287beccf40 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 11:20:28 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(sdlc-gate):=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8C=85=E9=96=98=E9=96=80=E5=8A=A0=E4=B8=8A=E6=AD=B8=E5=B1=AC?= =?UTF-8?q?=E6=AF=94=E5=B0=8D=E8=88=87=E9=A0=98=E5=8F=96=E7=B4=80=E9=8C=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:`hooks/sdlc-gate.sh` 新增 `wp-claim {owner}/{repo} {工作包代號} [{PR 編號}] [{分析頁}]` 與 `wp-unclaim {owner}/{repo}` 兩個子命令,`wp-lock` 新增第四個參數收工作包代號,鎖檔改為純文字 `key=value`,`wp-report` 多印第四欄工作包代號。`wp-check` 逐筆判歸屬:`prompt` 多注入一行點名不屬於這裡的 PR,`skill` 擋下 `plan`、`analyze`、`maintain` 時一併點名,`implement` 仍放行但收到同一則提醒。 Why:同一份分析常有好幾包平行進行,每包各自的 worktree 與 PR。原本的鎖檔只記存取庫與 PR 編號,看不出那支 PR 是誰的,於是任何一個工作階段都可能去改別包的程式碼、回別包的留言。 How:歸屬只比一件事——鎖檔的 `wp` 與同一個存取庫領取檔的 `wp` 取數字比一次,兩邊都有值且不相等就是別包的。`WP-03`、`WP-3`、`3` 先正規化再比,不然同一包的幾種寫法會被當成不同包。任何一邊查不到(沒有分析頁、沒寫代號、領取檔不存在)一律當查無歸屬並放行,理由與 `version-guard.sh` 一致:只擋確定違規,否則會把技能組維護自己鎖死。舊版單行 TSV 鎖檔照樣讀得動,讀出來是查無歸屬,換格式不會讓既有的鎖失效。`implement` 一律放行,連別包的 PR 未結清也放行——結清 PR 正是 `implement` 的步驟,擋它會把流程鎖死。分類迴圈用 here-document 餵資料而不用管線,管線右邊是子 shell,判到的違規會在迴圈結束時全部消失。 Who:跑 SDLC 實作階段的每個工作階段,以及 `jsc-sdlc/tools/wp-gate.sh` 的 `claim`、`lock` 與 `owns`。 --- hooks/sdlc-gate.sh | 159 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 144 insertions(+), 15 deletions(-) diff --git a/hooks/sdlc-gate.sh b/hooks/sdlc-gate.sh index cafc507..4208e0f 100755 --- a/hooks/sdlc-gate.sh +++ b/hooks/sdlc-gate.sh @@ -17,12 +17,19 @@ # sdlc-gate.sh check hook 模式(UserPromptSubmit):模型不符即擋下該輪提示。 # sdlc-gate.sh report 印出 {sid} {stage} {必要標籤} {上鎖時的模型};無鎖不印。 # -# sdlc-gate.sh wp-lock {owner}/{repo} {index} 記下一筆未結清的工作包 PR。 +# sdlc-gate.sh wp-lock {owner}/{repo} {index} [{工作包代號}] +# 記下一筆未結清的工作包 PR;第四個參數是這支 PR 做的是哪一包, +# 省略就是「查無歸屬」,之後的歸屬比對一律放行。 # exit 0 = 已記下;exit 2 = 用法錯誤或寫不進狀態檔(沒記下等於沒鎖)。 # 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-claim {owner}/{repo} {工作包代號} [{PR 編號}] [{分析頁}] +# 領取工作包:記下這個存取庫目前歸誰做,供歸屬比對用。 +# 由 jsc-sdlc 在領取工作包時呼叫,本檔不自己判斷歸誰。 +# exit 0 = 已記下;exit 2 = 用法錯誤或寫不進狀態檔。 +# sdlc-gate.sh wp-unclaim {owner}/{repo} 交回工作包,移除領取紀錄;檔案不存在也算成功。 +# 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。 @@ -34,15 +41,42 @@ # 跟「implement 挑下一個工作包能不能挑到某一包」是兩件事——後者的判斷依據是該包在分析頁 # WBS 表的相依欄,走 jsc-sdlc/tools/wp-gate.sh check-deps,不靠這支鎖檔。 # +# --- 狀態檔格式(與 jsc-sdlc/tools/wp-gate.sh 對齊,兩邊都靠這段註解對格式) --- +# +# 兩種檔案都放在 $JSC_HOME/wp/ 底下,都是純文字 key=value,一行一欄位,順序不拘, +# 不認得的鍵一律忽略。格式刻意做到最簡,兩邊各自實作也對得上。 +# +# 鎖檔 {owner}-{repo}-{index}.pr 一支未結清的工作包 PR +# repo={owner}/{repo} 存取庫 +# index={PR 編號} PR 編號,純數字 +# wp={工作包代號} 這支 PR 做的是哪一包,值取自分析頁 WBS 表那一欄的原字串; +# 查不到就留空 +# locked={ISO 時間} 上鎖時間,UTC +# +# 領取檔 {owner}-{repo}.claim 這個存取庫目前領取中的工作包,一個存取庫一支 +# repo={owner}/{repo} 存取庫 +# wp={工作包代號} 目前領取的是哪一包 +# pr={PR 編號} 這一包的 PR;還沒開 PR 就留空 +# analyze={分析頁頁名} 歸屬認定的來源分析頁;只作記錄,本檔不去讀那一頁 +# claimed={ISO 時間} 領取時間,UTC +# +# 舊版鎖檔是單行 TSV「{repo}{index}{上鎖時間}」,沒有工作包欄位。舊檔照樣讀得動, +# 讀出來的歸屬是空的,也就是查無歸屬、一律放行——不會因為換格式就把既有的鎖判成違規。 +# +# 歸屬比對只做一件事:把鎖檔的 wp 跟同一個存取庫領取檔的 wp 比數字。兩邊都有值且不相等, +# 那支 PR 就不屬於目前領取的工作包。任何一邊查不到(沒有分析頁、沒寫工作包代號、領取檔不存在) +# 一律當查無歸屬並放行,理由與 version-guard.sh 相同:只擋確定違規,否則會把技能組維護鎖死。 +# # exit code 例外:其他 jsc hook 一律 exit 0 不中斷宿主 CLI;本檔 check 與 wp-check skill 是 # 刻意的例外——鎖存在且不合規時 exit 2 擋下。只用提示注入的話模型可以無視,閘門形同虛設。 # 無鎖、或資料不足無法判定時,仍照舊 exit 0 安靜降級。 HERE=$(dirname "$0"); . "$HERE/lib.sh" # 只有需要 stdin JSON 的子命令才讀它:模型判定要 transcript_path,session 判定要 session_id。 -# wp-lock、wp-unlock、wp-report 兩者都不需要,而 read_stdin 在標準輸入是管線又沒人關閉時會 -# 一直等——工具腳本(jsc-sdlc 的 wp-gate.sh)轉呼叫這三個子命令時就這樣整支卡死。 +# wp-lock、wp-unlock、wp-claim、wp-unclaim、wp-report 兩者都不需要,而 read_stdin 在標準輸入 +# 是管線又沒人關閉時會一直等——工具腳本(jsc-sdlc 的 wp-gate.sh)轉呼叫這些子命令時就這樣整支 +# 卡死。新增子命令一律照這個原則歸類:不需要 stdin 就加進下面這一列。 case "${1:-}" in - wp-lock|wp-unlock|wp-report) STDIN_JSON="" ;; + wp-lock|wp-unlock|wp-claim|wp-unclaim|wp-report) STDIN_JSON="" ;; *) read_stdin ;; esac sid=$(session_id) @@ -129,17 +163,65 @@ wp_state_file() { # $1={owner}/{repo} $2=index printf '%s/%s-%s.pr' "$WP_DIR" "$(printf '%s' "$1" | tr '/' '-')" "$2" } -# 未結清清單,每行「{owner}/{repo} {index} {上鎖時間}」;沒有就不輸出。 +wp_claim_file() { # $1={owner}/{repo} + printf '%s/%s.claim' "$WP_DIR" "$(printf '%s' "$1" | tr '/' '-')" +} + +# 取 key=value 檔案裡某個鍵的值;沒有那個鍵就不輸出。值裡的等號原樣保留。 +wp_field() { # $1=檔案 $2=鍵名 + [ -f "$1" ] || return 0 + awk -v k="$2" ' + index($0, k "=") == 1 { + v = substr($0, length(k) + 2) + gsub(/^[ \t]+|[ \t\r]+$/, "", v) + print v; exit + }' "$1" 2>/dev/null +} + +# 讀一支鎖檔,印出「{owner}/{repo} {index} {上鎖時間} {工作包代號}」。 +# 新格式(key=value)與舊格式(單行 TSV,無工作包欄位)都認:換格式不該讓既有的鎖失效。 +wp_read_lock() { # $1=鎖檔 + [ -f "$1" ] || return 0 + awk ' + /^[a-z][a-z]*=/ { + k = substr($0, 1, index($0, "=") - 1) + v = substr($0, index($0, "=") + 1) + gsub(/^[ \t]+|[ \t\r]+$/, "", v) + f[k] = v; kv = 1; next + } + NR == 1 { split($0, t, "\t") } + END { + if (kv) { repo = f["repo"]; idx = f["index"]; at = f["locked"]; wp = f["wp"] } + else { repo = t[1]; idx = t[2]; at = t[3]; wp = "" } + gsub(/\r/, "", at) + if (repo != "") print repo " " idx " " at (wp != "" ? " " wp : "") + }' "$1" 2>/dev/null +} + +# 未結清清單,每行「{owner}/{repo} {index} {上鎖時間} {工作包代號}」;沒有就不輸出。 +# 第四欄可能是空的,代表查無歸屬。 wp_pending() { [ -d "$WP_DIR" ] || return 0 for _f in "$WP_DIR"/*.pr; do [ -f "$_f" ] || continue - _line=$(sed -n '1p' "$_f" 2>/dev/null | tr '\t' ' ') + _line=$(wp_read_lock "$_f") [ -n "$_line" ] && printf '%s\n' "$_line" done } -# 未結清清單濃縮成一句可讀的「{repo} 第 {index} 號」,多筆用頓號串起。 +# 某個存取庫目前領取中的工作包代號;沒有領取檔或沒寫代號就不輸出(查無歸屬)。 +wp_claimed() { # $1={owner}/{repo} + wp_field "$(wp_claim_file "$1")" wp +} + +# 工作包代號取數值:去掉英文前綴與前導零,只留數字。範本補零到兩位,呼叫端不一定補, +# 也可能只給數字;比字串會讓同一包的幾種寫法互相認不得,歸屬就誤判成不同包。 +wp_num() { # $1=工作包代號 + printf '%s' "${1:-}" | tr -dc '0-9' | sed 's/^0*//' +} + +# 未結清清單濃縮成一句可讀的「{repo} 第 {index} 號」,多筆用頓號串起。只吃前兩欄, +# 第四欄的工作包代號另外由歸屬比對處理,混進這句話只會把提醒句拉長。 wp_brief() { # 標準輸入 = wp_pending 的輸出 awk '{ out = (out == "" ? $1 " 第 " $2 " 號" : out "、" $1 " 第 " $2 " 號") } END { print out }' } @@ -232,7 +314,7 @@ case "${1:-check}" in exit 0 ;; wp-lock) - repo="${2:-}"; idx="${3:-}" + repo="${2:-}"; idx="${3:-}"; wp="${4:-}" wp_valid_repo "$repo" || { echo "[jsc][工作包閘門][ERR]:存取庫須為 {owner}/{repo} 格式,收到「${repo:-空值}」。" >&2; exit 2; } case "$idx" in @@ -241,9 +323,32 @@ case "${1:-check}" in esac mkdir -p "$WP_DIR" 2>/dev/null || true wpf=$(wp_state_file "$repo" "$idx") - printf '%s\t%s\t%s\n' "$repo" "$idx" "$(now_iso)" > "$wpf" 2>/dev/null || { + printf 'repo=%s\nindex=%s\nwp=%s\nlocked=%s\n' "$repo" "$idx" "$wp" "$(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 未結清${wp:+(工作包 $wp)}。相依於它的工作包在它結清前不得開始;其餘互不相依的工作包不受影響。" + [ -n "$wp" ] || echo "[jsc][工作包閘門]:這筆沒帶工作包編號,歸屬比對查不到來源,之後一律放行。要擋跨工作包的 PR,請在 wp-lock 帶上第四個參數。" + exit 0 ;; + + wp-claim) + repo="${2:-}"; wp="${3:-}"; pr="${4:-}"; page="${5:-}" + wp_valid_repo "$repo" || { + echo "[jsc][工作包閘門][ERR]:存取庫須為 {owner}/{repo} 格式,收到「${repo:-空值}」。" >&2; exit 2; } + [ -n "$(wp_num "$wp")" ] || { + echo "[jsc][工作包閘門][ERR]:工作包代號須帶數字(例:分析頁上那個 WP 開頭的代號),收到「${wp:-空值}」。" >&2; exit 2; } + mkdir -p "$WP_DIR" 2>/dev/null || true + cf=$(wp_claim_file "$repo") + printf 'repo=%s\nwp=%s\npr=%s\nanalyze=%s\nclaimed=%s\n' \ + "$repo" "$wp" "$pr" "$page" "$(now_iso)" > "$cf" 2>/dev/null || { + echo "[jsc][工作包閘門][ERR]:寫不進領取檔 $cf,歸屬比對不會生效。" >&2; exit 2; } + echo "[jsc][工作包閘門][OK]:已記下 $repo 領取中的工作包為 $wp。這個存取庫的其他工作包 PR 一律不由這裡結清。" + exit 0 ;; + + wp-unclaim) + repo="${2:-}" + wp_valid_repo "$repo" || { + echo "[jsc][工作包閘門][ERR]:存取庫須為 {owner}/{repo} 格式,收到「${repo:-空值}」。" >&2; exit 2; } + # 冪等,理由同 wp-unlock:交回流程可能被重跑,第二次失敗只會讓呼叫端誤判。 + rm -f "$(wp_claim_file "$repo")" 2>/dev/null || true exit 0 ;; wp-unlock) @@ -267,6 +372,24 @@ case "${1:-check}" in pending=$(wp_pending) [ -n "$pending" ] || exit 0 brief=$(printf '%s\n' "$pending" | wp_brief) + + # 逐筆判歸屬:鎖檔的工作包代號對上同一個存取庫領取中的代號,才算自己這一包的 PR。 + # 兩邊任一邊查不到代號就算查無歸屬,歸進 mine 這一側處理——查不到不是違規的證據。 + # 迴圈用 here-document 餵資料,不用管線:管線右邊是子 shell,分類結果傳不回來, + # 判到的違規會在迴圈結束的瞬間全部消失。 + mine=''; foreign='' + while read -r _r _i _t _w; do + [ -n "$_r" ] || continue + _a=$(wp_num "$_w"); _b=$(wp_num "$(wp_claimed "$_r")") + if [ -n "$_a" ] && [ -n "$_b" ] && [ "$_a" != "$_b" ]; then + foreign="${foreign}${foreign:+、}$_r 第 $_i 號($_w)" + else + mine="${mine}${mine:+、}$_r 第 $_i 號" + fi + done <&2 + echo "[jsc][工作包閘門][ERR]:${brief} PR 尚未合併,禁止在此存取庫執行「${sname}」。${foreign:+其中 ${foreign}還不屬於領取中的工作包,更不該由這裡處理。}請先把該 PR 結清(合併或關閉),或執行 jsc-hooks/hooks/sdlc-gate.sh wp-unlock {owner}/{repo} {index} 解除;確定要整體放行請設 JSC_WP_GATE=off。本次技能呼叫已擋下。" >&2 exit 2 ;; + implement) + # implement 一律放行,連別包的 PR 未結清也放行:結清 PR 正是 implement 步驟 4 要做 + # 的事,擋掉就沒有任何路徑能解除這道鎖。領取檔不綁工作階段,擋下去連領取那一包的 + # 工作階段都會被自己的舊紀錄擋住,等於把流程鎖死。所以這裡只注入歸屬提醒。 + [ -n "$foreign" ] && echo "[jsc][工作包閘門]:${foreign}不屬於這個存取庫領取中的工作包。這裡只結清自己領取那一包的 PR${mine:+(${mine})};別包的 PR 不要改、留言也不要回,交給領取它的工作階段。" + exit 0 ;; esac - # implement 與其餘技能一律放行:結清 PR 正是 implement 步驟 4 要做的事, - # 擋掉 implement 就沒有任何路徑能解除這道鎖,等於把流程鎖死。 + # 其餘技能一律放行:這道閘門管的是 SDLC 階段,不是整台機器的技能呼叫。 exit 0 ;; esac exit 0 ;; From 3b6dd1b9011ee8c65038c701deae2f4b05c81ac7 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 11:20:28 +0800 Subject: [PATCH 2/4] =?UTF-8?q?test(wire-cli):=20=E5=86=92=E7=85=99?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6=E8=A3=9C=E9=A9=97=E5=B7=A5=E4=BD=9C=E5=8C=85?= =?UTF-8?q?=E6=AD=B8=E5=B1=AC=E7=9A=84=E5=9B=9B=E6=A2=9D=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E8=B7=AF=E5=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:`tools/wire-cli.sh` 的 `smoke` 新增一段,用一份暫時的 `$JSC_HOME` 狀態檔把「狀態檔不存在」「PR 屬於領取中的工作包」「PR 屬於別的工作包」「逃生門 `JSC_WP_GATE=off`」四種情境各跑一次,逐一比對結束碼;收尾那句 `status=ok` 的說明也跟著補上這一段。 Why:原本兩支 `wp-check` 冒煙都把技能名清空,只走得到「沒有未結清 PR」與「取不到技能名」兩條捷徑,歸屬比對整段一次都沒跑到。判定寫了卻沒驗,等於沒寫。 How:驗的是判定結果本身,不只是腳本跑得完——每一種情境都給定預期結束碼,對不上就計入失敗並印出前 200 字的輸出。狀態檔放在暫時目錄,冒煙測試不該在使用者真正的 `$JSC_HOME/wp/` 留下痕跡。每次呼叫都接 `&1); _rc=$? + if [ "$_rc" -eq "$3" ]; then + printf '[jsc] sdlc-gate.sh wp-check skill(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] sdlc-gate.sh wp-check skill(%s):exit %s,預期 %s,歸屬判定壞了:%s\n' \ + "$1" "$_rc" "$3" "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out" + fi + } + if wp_home=$(mktemp -d 2>/dev/null) && mkdir -p "$wp_home/wp" 2>/dev/null; then + smoke_wp_case "狀態檔不存在" plan 0 + printf 'repo=jsc/smoke\nwp=WP-03\npr=12\n' > "$wp_home/wp/jsc-smoke.claim" + printf 'repo=jsc/smoke\nindex=12\nwp=WP-03\n' > "$wp_home/wp/jsc-smoke-12.pr" + smoke_wp_case "PR 屬於領取中的工作包,技能 implement" implement 0 + printf 'repo=jsc/smoke\nindex=9\nwp=WP-01\n' > "$wp_home/wp/jsc-smoke-9.pr" + smoke_wp_case "PR 屬於別的工作包,技能 plan" plan 2 + smoke_wp_case "PR 屬於別的工作包,技能 implement" implement 0 + smoke_wp_case "逃生門 JSC_WP_GATE=off" plan 0 off + rm -rf "$wp_home" + else + smoke_fails=$((smoke_fails + 1)) + printf '[jsc] sdlc-gate.sh wp-check skill:建不出暫存目錄,工作包歸屬判定沒驗到。\n' >> "$smoke_out" + fi # comment-scope.sh 有三個接在不同事件的模式,三個都要驗:prompt 一律 exit 0, # 無參數模式在取不到檔名時安靜 exit 0(上面已清空 JSC_CHANGED_FILE,stdin 也只有 {}), # sweep 掃目前工作目錄所在的 git 工作區——乾淨或非 git 目錄回 0,有違規註解回 2, @@ -682,7 +715,7 @@ if [ "$action" = smoke ]; then smoke_one lang-guard.sh sweep if [ "$smoke_fails" -eq 0 ]; then - printf 'status=ok reason=%s\n' "七支 hook 的每個接線模式都跑得完,沒有執行期錯誤" + printf 'status=ok reason=%s\n' "七支 hook 的每個接線模式都跑得完,工作包歸屬判定也各走過一次,沒有執行期錯誤" cat "$smoke_out"; rm -f "$smoke_out"; exit 0 fi printf 'status=failed reason=%s\n' "$smoke_fails 支 hook 有執行期錯誤" From 29facd385cead46c5de0c467f1c15c981ce23ef5 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 11:20:28 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs(hooks):=20README=20=E8=A3=9C=E4=B8=8A?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=85=E6=AD=B8=E5=B1=AC=E7=8B=80=E6=85=8B?= =?UTF-8?q?=E6=AA=94=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:README 新增「工作包歸屬狀態檔」一節,用表列出鎖檔與領取檔的檔名、欄位與範例,寫明誰寫誰讀、查無歸屬一律放行、舊版 TSV 鎖檔照樣讀得動;`sdlc-gate.sh` 與 `wire-cli.sh` 兩列的說明同步補上新子命令與冒煙測試的新內容。 Why:狀態檔的格式是 `jsc-hooks` 與 `jsc-sdlc` 兩邊的介面。介面只寫在腳本註解裡,另一邊改的時候看不到,格式一走鐘歸屬就全部誤判。 How:格式壓到最簡的純文字 `key=value`,一行一欄位,順序不拘,不認得的鍵一律忽略,兩邊各自實作也對得上。表只寫欄位與範例,判定規則寫在表底下的三段短說明。 Who:讀 `jsc-hooks` 說明的人,以及 `jsc-sdlc` 那一側寫 `wp-gate.sh` 的人。 --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 481c135..fd7ba06 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 | `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 | | `hooks/comment-scope.sh` | UserPromptSubmit、PostToolUse(Write、Edit、MultiEdit)、codex `notify`、kiro `userPromptSubmit`、`tools/jsc-wrap.sh` 收尾 | 程式碼註解不得夾帶文件相關資訊,共三種模式。`prompt`:在每次提示注入規則摘要(禁止項與白名單各一行),五個 CLI 都接得到。無參數:寫檔後的逐檔掃描,從 stdin JSON 取 `file_path`(或環境變數 `JSC_CHANGED_FILE`),只有 claude 的 PostToolUse 接得上。`sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,給沒有 post-tool hook 的四個 CLI 用,找不到 git 就安靜 exit 0。掃描時機每個 CLI 不同——claude 逐檔即時(PostToolUse)、codex 每輪結束(`notify`)、kiro 每輪提示送出時(`userPromptSubmit`,掃的是上一輪寫的檔)、copilot 與 antigravity 只有工作階段結束時由 `tools/jsc-wrap.sh` 收尾掃一次。兩種掃描模式都只看 `git diff HEAD` 的新增行、不翻舊帳,命中就把警告與最多三行證據送到 stderr 並以 exit 2 交回模型就地修正(不擋寫入,檔案已經寫好了)。markdown、純文字、資料檔與二進位檔一律跳過。只實作可用樣式判定的項目,專案代號、客戶名稱這類判不出來的交給 `/jsc-review:code-review`。規則正文的唯一來源在 `jsc-review` 的 `references/comment-scope.md`,本存取庫不留副本。逃生門 `JSC_COMMENT_SCOPE=off` | | `hooks/lang-guard.sh` | UserPromptSubmit、PostToolUse(Write、Edit、MultiEdit)、codex `notify`、kiro `userPromptSubmit`、`tools/jsc-wrap.sh` 收尾 | 所有非程式碼輸出一律繁體中文、UTF-8、無亂碼、無簡體字,共三種模式。`prompt`:在每次提示注入規則摘要(適用範圍與自我檢查各一行),五個 CLI 都接得到。無參數:寫檔後的逐檔掃描,從 stdin JSON 取 `file_path`(或環境變數 `JSC_CHANGED_FILE`),只有 claude 的 PostToolUse 接得上。`sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,給沒有 post-tool hook 的四個 CLI 用,找不到 git 就安靜 exit 0。接線位置與掃描時機跟 `comment-scope.sh` 完全一樣,見下面那張表。偵測三項:簡體字(字表在 `hooks/simplified.txt`,讀不到就安靜跳過這一項)、亂碼(U+FFFD 替代字元與雙重編碼殘骸)、非 UTF-8 編碼(用 `iconv` 判定,沒有 `iconv` 就跳過)。三項都掃整個檔案、不只掃註解行,`.md` 與純文字檔照掃——那些正是「非程式碼輸出」的主場,這兩點跟 `comment-scope.sh` 刻意不同。掃描深度仍只看 `git diff HEAD` 的新增行、不翻舊帳,命中就把警告與最多三行證據送到 stderr 並以 exit 2 交回模型就地修正(不擋寫入)。二進位檔(只認 NUL 位元組)與 `*.lock`、`*.min.js`、`*.map` 這類產生檔跳過;`hooks/simplified.txt`、`hooks/ste100-guard.sh`、`hooks/lang-guard.sh` 也跳過,那三份檔案裡的簡體字與亂碼樣本是被討論的對象,不是被使用。規則正文的唯一來源在 `jsc-meta` 的 `references/ste100.md`。逃生門 `JSC_LANG_GUARD=off` | -| `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` 查證 | +| `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-claim {owner}/{repo} {工作包代號} [{PR 編號}] [{分析頁頁名}]` 記下這個存取庫目前領取哪一包、`wp-unclaim {owner}/{repo}` 交回、`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`,不是這裡。另外會比對歸屬:未結清的 PR 不屬於目前領取的工作包時,`prompt` 多注入一行「那幾支交給領取它的工作階段」,`skill` 在擋下 `plan`、`analyze`、`maintain` 時一併點名,`implement` 仍放行但收到同一則提醒。逃生門 `JSC_WP_GATE=off`。這道閘門只讀檔案、不打網路,PR 的真實合併狀態由 `jsc-sdlc/tools/wp-gate.sh` 查證 | Claude 由 `hooks/hooks.json` 自動接線七支 hook;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。 @@ -45,6 +45,19 @@ Claude 由 `hooks/hooks.json` 自動接線七支 hook;其他 CLI 用 `hooks-in > 上表對 `comment-scope.sh` 與 `lang-guard.sh` 同時成立,兩支接在同一批位置。`sweep` 看的是 `git diff HEAD`,涵蓋範圍與 claude 一樣,差的是回饋速度:claude 當下就叫,其他四個要等到該輪或該階段結束。不在 git 工作區內時 `sweep` 安靜 exit 0,等於沒掃。規則提示(`prompt` 模式)在五個 CLI 都照樣寫進規則檔,三段(STE100、註解範圍、繁中編碼)共用同一個標記段落——晚一輪的警告,價值仍低於一開始就不要寫。判不出來的項目(專案代號、客戶名稱)一律交給 `/jsc-review:code-review` 第 2 組。 +### 工作包歸屬狀態檔 + +`$JSC_HOME/wp/` 底下兩種檔案,都是純文字 `key=value`,一行一欄位,順序不拘,不認得的鍵一律忽略。格式壓到最簡,`jsc-hooks` 與 `jsc-sdlc` 兩邊各自實作也對得上。 + +| 檔案 | 欄位 | 範例(一行一欄位) | +| --- | --- | --- | +| 鎖檔 `{owner}-{repo}-{index}.pr` | `repo`、`index`、`wp`、`locked` | `repo=jsc/demo`、`index=12`、`wp=WP-03`、`locked=2026-08-27T02:00:00Z` | +| 領取檔 `{owner}-{repo}.claim` | `repo`、`wp`、`pr`、`analyze`、`claimed` | `repo=jsc/demo`、`wp=WP-03`、`pr=12`、`analyze=ANALYZE_1A2B3C4D`、`claimed=2026-08-27T02:00:00Z` | + +寫檔的一律是 `jsc-sdlc`(領工作包時呼叫 `wp-claim`,開完 PR 呼叫 `wp-lock`),hook 只讀檔比對:把鎖檔的 `wp` 和同一個存取庫領取檔的 `wp` 取數字比一次,兩邊都有值且不相等,那支 PR 就不是這裡該處理的。 + +歸屬查不到就放行(exit 0,只注入提醒):沒有分析頁、`wp` 沒寫、領取檔不存在,三種都算這一類。理由與 `version-guard.sh` 一致——只擋確定違規,否則會把技能組維護自己鎖死。舊版鎖檔是單行 TSV(`{repo}{index}{上鎖時間}`,沒有工作包欄位),照樣讀得動,讀出來是查無歸屬。 + > `version-guard.sh report` 是非 hook 的子指令:印出每個已安裝 jsc plugin 的 > 「{domain} {本機} {遠端} {落後|最新|超前|查詢失敗}」,最後一行 `behind {落後個數}`。 > 本機沒有 Claude 的 plugin 註冊檔時改印 `noregistry {路徑}` 再接 `behind 0`, @@ -59,7 +72,7 @@ Claude 由 `hooks/hooks.json` 自動接線七支 hook;其他 CLI 用 `hooks-in | `tools/jsc-wrap.sh` | 沒有完整 hook 系統的 CLI 的包裝啟動器:匯出 `JSC_CLI`、`JSC_SESSION_ID`,前後接 `session-timer.sh`,結束時自動跑 `scan-logs.sh` 回填,再依序跑一次 `comment-scope.sh sweep` 與 `lang-guard.sh sweep` 掃整個 git 工作區的註解範圍與繁中編碼(copilot 與 antigravity 沒有任何逐輪事件,整個工作階段只有這裡掃得到)。兩次收尾掃描一律不影響結束碼:包裝器原樣回傳 CLI 自己的結束碼,`sweep` 命中只把警告印到 stderr。`JSC_CLI` 存 CLI 代號,實際執行的是對應的執行檔(antigravity 是 agy、kiro 是 kiro-cli) | | `tools/scan-logs.sh` | 離線回填:解析 copilot、antigravity、codex 的原生日誌,把技能用量與階段界線補進 `$JSC_HOME`,重掃不重複 | | `tools/report-error.sh` | 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 `ERROR_{HASH}`,並在 `ERROR_CONTENTS` 附上一列索引。wiki 位置由 `jsc-gitea` 的 `gitea.sh wiki-repo ERROR` 解析,解析不出來就安靜降級。由操作者手動執行,或由 `hooks-install` 在 `wire-cli.sh` 回報 `status=failed` 時執行;**不接在失敗的 hook 上自動觸發**(hook 一律安靜 exit 0,自我回報會疊出迴圈) | -| `tools/wire-cli.sh` | 單一 CLI 的 hook 生命週期,共三個用法。`{cli}` 是接線:對應的設定編輯、包裝別名安裝、hook 檔建立,皆以 ``(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝;寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、kiro 的 JSON 必須成對且 `on`、`run` 在最上層),以 `status=wired\|degraded\|skipped\|failed` 回報。`purge {cli}` 是移除:把該 CLI 的**所有** hook 清掉,含非 jsc 的第三方項目,動到的檔案先原樣備份到 `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`,備份失敗就不移除,移除後重讀驗證,驗不過自動還原備份,以 `status=purged\|skipped\|failed` 回報。`smoke {cli}` 是執行期冒煙測試:七支 hook 的每個接線模式各跑一次,非零退出即為錯誤(例外有三個:`sdlc-gate.sh check` 的 exit 2 是階段鎖的設計行為,`comment-scope.sh` 與 `lang-guard.sh` 掃描模式的 exit 2 是掃到違規的設計行為——`sweep` 在髒工作區本來就會回 2,不算 hook 壞掉),以 `status=ok\|failed` 回報。`status {cli}` 是唯讀盤點:只讀設定檔判斷標記段落在不在,不寫檔也不執行 hook,每個接線點印一行 `item{項目}{路徑}{present\|missing}`,以 `status=wired\|degraded\|unwired\|skipped` 回報(結束碼 0、1、5、3)。體檢類技能(`/jsc-cli:doctor`)只能用這個子命令,另外三個都會動到環境 | +| `tools/wire-cli.sh` | 單一 CLI 的 hook 生命週期,共三個用法。`{cli}` 是接線:對應的設定編輯、包裝別名安裝、hook 檔建立,皆以 ``(或 `# jsc-hooks`)標記整段重寫,重跑等同先移除再重裝;寫完每個檔案會重讀驗證位置正確才回報成功(codex 的 `notify` 必須是根層鍵、kiro 的 JSON 必須成對且 `on`、`run` 在最上層),以 `status=wired\|degraded\|skipped\|failed` 回報。`purge {cli}` 是移除:把該 CLI 的**所有** hook 清掉,含非 jsc 的第三方項目,動到的檔案先原樣備份到 `$JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/`,備份失敗就不移除,移除後重讀驗證,驗不過自動還原備份,以 `status=purged\|skipped\|failed` 回報。`smoke {cli}` 是執行期冒煙測試:七支 hook 的每個接線模式各跑一次,非零退出即為錯誤,另外用一份暫時的 `$JSC_HOME` 狀態檔把工作包歸屬的四條判定路徑(查無歸屬、自己的工作包、別的工作包、逃生門)各跑一次並比對結束碼,驗的是判定結果本身,不只是腳本跑得完(例外有三個:`sdlc-gate.sh check` 的 exit 2 是階段鎖的設計行為,`comment-scope.sh` 與 `lang-guard.sh` 掃描模式的 exit 2 是掃到違規的設計行為——`sweep` 在髒工作區本來就會回 2,不算 hook 壞掉),以 `status=ok\|failed` 回報。`status {cli}` 是唯讀盤點:只讀設定檔判斷標記段落在不在,不寫檔也不執行 hook,每個接線點印一行 `item{項目}{路徑}{present\|missing}`,以 `status=wired\|degraded\|unwired\|skipped` 回報(結束碼 0、1、5、3)。體檢類技能(`/jsc-cli:doctor`)只能用這個子命令,另外三個都會動到環境 | | `tools/scan-hook-errors.sh` | 掃 CLI 原生紀錄找 hook 的執行期錯誤(接線寫對、跑起來出錯)。只有 claude 有 hook 結果紀錄,掃 `~/.claude/projects/**/*.jsonl` 的 `hook_non_blocking_error` 與非空 `hookErrors`;codex、copilot、antigravity、kiro 沒有等價紀錄,一律回報 `unavailable` 並指向 `wire-cli.sh smoke {cli}`。每筆錯誤附加一行 JSON 到 `$JSC_HOME/errors/hooks.jsonl`,`jsc` 欄位標明是不是 jsc 自己的 hook(第三方 hook 的錯誤只回報,不由 jsc 修正);去重與 `scan-logs.sh` 同法,重掃只讀新增段落,以 `status=clean\|errors\|unavailable` 回報 | ## 失敗回報範本 From be75cc4bedba1610bdacebbf86ef5f7fbfc8f697 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 11:20:28 +0800 Subject: [PATCH 4/4] =?UTF-8?q?chore(manifest):=20=E4=B8=89=E4=BB=BD=20man?= =?UTF-8?q?ifest=20=E7=89=88=E6=9C=AC=E5=8D=87=E5=88=B0=200.2.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:`plugin.json`、`.claude-plugin/plugin.json`、`.codex-plugin/plugin.json` 的 `version` 由 0.2.2 改為 0.2.3。 Why:本次新增 `wp-claim`、`wp-unclaim` 兩個子命令並改了鎖檔格式,屬於行為變更,版本要跟著往上走,各 CLI 才知道要更新。 How:三份只改 `version` 一個欄位,其餘內容不動,三份保持同一版號。 Who:`jsc-hooks` 外掛的套件描述檔。 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 09c2b65..c7339a4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.2.2", + "version": "0.2.3", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills", "author": { diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 56a1664..bf89b9d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.2.2", + "version": "0.2.3", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills" } diff --git a/plugin.json b/plugin.json index 2290baf..3e2fbc3 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-hooks", - "version": "0.2.2", + "version": "0.2.3", "description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖、版本前置檢查", "skills": "./skills/" }