異常回報依 wiki 讀取的結束碼分流,只有頁面確定不存在才套範本建新頁。 版本閘門與重啟閘門的豁免清單各補上 hook 修復技能。 階段閘門把計畫階段移出擋人名單,改成只注入提醒。 錯誤掃描的自家 hook 判定補齊九支腳本,並加一條路徑判定。 修復技能與接線技能的內文改成真的走得到的路徑與真的存在的關卡數。 原本 wiki 讀取失敗會一路落到套範本那一步,金鑰失效或 API 出狀況時, 就拿一份空白範本蓋掉整份異常目錄,而寫入不做合併也不留備份,蓋掉就救不回來。 兩道閘門把唯一的 hook 修復路徑一起擋住,hook 一壞就沒有任何方法修回來, 閘門等於鎖掉解除自己的路徑。工作包閘門擋下計畫階段是誤擋: 計畫是純邏輯階段、不碰程式碼,而閘門只知道有 PR 未合併,判不出跟新計畫有沒有關聯。 自家 hook 判定只認得早期那五支,後來加的四支出錯會被當成第三方的,只回報不修正。 修復技能裡三個指向流程的路徑指到不存在的位置,照著走一定撲空; 接線流程寫四道關卡,實際上有五道,兩段中文說明也混在英文內文裡。 異常目錄改成先讀回舊頁、把新列附在文末、再整頁寫回;讀不回來就放棄寫目錄頁並回報, 寧可少一列索引,也不覆蓋別人的紀錄。兩份豁免清單各補一項,理由逐項寫在腳本檔頭。 計畫階段改印提醒後放行,放棄的在製品上限與代價一併寫在檔頭。 自家 hook 判定逐支列出腳本名,再加一條安裝路徑判定,日後新增 hook 忘了補清單也還認得出來。 三個路徑改指到擁有它的技能組,關卡數改成五道並逐關寫明結束碼,兩段中文說明改回英文。 版本閘門在同一次改動另補唯讀的建議子命令,把版本比對表收斂成一行結論, 部署技能不必自己再解一次那張表。
484 lines
27 KiB
Bash
Executable File
484 lines
27 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
# sdlc-gate.sh — SDLC 階段模型閘門與 session 模型鎖(stage ∈ plan/analyze/implement/maintain)。
|
||
#
|
||
# 判準是階段的「能力標籤」,不是模型名稱:
|
||
# - 標籤資料讀 $JSC_HOME/model-tags.tsv,由 jsc-cli/tools/model-tags.sh sync 產生。
|
||
# - 目前模型 id 取自可驗證紀錄。優先讀 transcript,其次讀 hook stdin JSON 與 Codex 本機
|
||
# session 記錄。只有這些來源都失敗時,才接受 JSC_MODEL 這個人工覆寫。
|
||
# - 不採用對話內容裡模型自稱的 id。自我回報無法驗證,等同沒有閘門。
|
||
# - 鎖存的是「階段的必要標籤」,不是「上鎖那一刻的模型」。鎖當時的模型只留作記錄,
|
||
# 否則用不合格的模型起跑就會把自己鎖成合格,閘門永遠通過。
|
||
#
|
||
# 狀態檔:$JSC_HOME/sessions/{sid}.stage,單行「{stage}<TAB>{必要標籤}<TAB>{上鎖時的模型}<TAB>{模型來源}」。
|
||
#
|
||
# 用法:
|
||
# sdlc-gate.sh lock {stage} 階段閘門:比對實際模型與該階段必要標籤,通過才上鎖。
|
||
# exit 0 = 通過並已上鎖;exit 1 = 未通過,呼叫端必須停止流程。
|
||
# sdlc-gate.sh unlock 移除狀態檔(被擋住又確定要放行時的逃生門)。
|
||
# sdlc-gate.sh check hook 模式(UserPromptSubmit):模型不符即擋下該輪提示。
|
||
# sdlc-gate.sh report 印出 {sid} {stage} {必要標籤} {模型 id} {模型來源} {判定};無鎖不印。
|
||
#
|
||
# 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-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):命中 analyze 或 maintain
|
||
# 時 exit 2 擋下該次呼叫;plan 與 implement 只注入提醒後 exit 0,
|
||
# 其餘技能一律 exit 0。
|
||
#
|
||
# 結束碼(一個子命令一列):
|
||
# lock 0=通過並已上鎖 1=未通過,呼叫端必須停止流程(階段名不合法、讀不到該
|
||
# 階段的必要標籤、判定不出目前模型、模型不在標籤表上、缺標籤、寫不進狀態檔)
|
||
# unlock 0=永遠成功,狀態檔不存在也算
|
||
# check 0=放行 2=擋下該輪提示。安靜放行:沒有階段鎖、舊格式狀態檔讀不出必要標籤、
|
||
# 必要標籤是 any、判定不出目前模型(只提醒不擋)
|
||
# report 0=永遠成功,無鎖就不印
|
||
# wp-lock 0=已記下 2=存取庫不是 {owner}/{repo}、PR 編號不是數字、寫不進狀態檔
|
||
# wp-unlock 0=已結清,狀態檔不存在也算 2=存取庫或 PR 編號格式錯誤
|
||
# wp-claim 0=已記下 2=存取庫格式錯誤、工作包代號不帶數字、寫不進領取檔
|
||
# wp-unclaim 0=已交回,領取檔不存在也算 2=存取庫格式錯誤
|
||
# wp-report 0=永遠成功,沒有未結清就不印
|
||
# wp-check prompt 0=永遠放行,只注入提醒
|
||
# wp-check skill 0=放行 2=擋下該次技能呼叫。安靜放行:逃生門 JSC_WP_GATE=off、沒有未結清
|
||
# 的工作包、取不到技能名、技能是 plan 或 implement、其餘不在名單上的技能。
|
||
# 只有 analyze 與 maintain 會 exit 2
|
||
# 不認得的子命令 0=安靜放行
|
||
# 註:本檔以 `. "$HERE/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會就地結束
|
||
# 並回 2;check 接在 UserPromptSubmit、wp-check skill 接在 PreToolUse,那一下都是無聲擋人,
|
||
# 上面那些放行路徑一條都跑不到。
|
||
#
|
||
# 鎖檔一個工作包一支($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,不靠這支鎖檔。
|
||
#
|
||
# --- plan 已從擋人名單移出,被放棄的保護寫在這裡 ---
|
||
#
|
||
# plan 原本跟 analyze、maintain 一起被擋。現在改成只提醒、照樣放行,放棄的是「在製品上限」:
|
||
# 手上的工作包還沒結清就不准開新計畫。放棄的理由有兩個。一是 plan 是純邏輯階段,產出是計畫頁,
|
||
# 不碰程式碼,開一份新計畫不會動到那支未結清的 PR。二是這道閘門手上只有「這個存取庫有 PR
|
||
# 未合併」這一個事實,判不出新計畫跟那支 PR 有沒有關聯,擋下去多半是誤擋。
|
||
# 代價要據實看待:沒有東西再擋住計畫越積越多,計畫的產出速度可以快過實作。
|
||
# analyze 與 maintain 兩道仍在,上限只是晚一個階段才生效。
|
||
#
|
||
# --- 狀態檔格式(與 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}<TAB>{index}<TAB>{上鎖時間}」,沒有工作包欄位。舊檔照樣讀得動,
|
||
# 讀出來的歸屬是空的,也就是查無歸屬、一律放行——不會因為換格式就把既有的鎖判成違規。
|
||
#
|
||
# 歸屬比對只做一件事:把鎖檔的 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-claim、wp-unclaim、wp-report 兩者都不需要,而 read_stdin 在標準輸入
|
||
# 是管線又沒人關閉時會一直等——工具腳本(jsc-sdlc 的 wp-gate.sh)轉呼叫這些子命令時就這樣整支
|
||
# 卡死。新增子命令一律照這個原則歸類:不需要 stdin 就加進下面這一列。
|
||
case "${1:-}" in
|
||
wp-lock|wp-unlock|wp-claim|wp-unclaim|wp-report) STDIN_JSON="" ;;
|
||
*) read_stdin ;;
|
||
esac
|
||
sid=$(session_id)
|
||
state="$JSC_HOME/sessions/$sid.stage"
|
||
TAGS_TSV="$JSC_HOME/model-tags.tsv"
|
||
STAGES="plan analyze implement maintain"
|
||
|
||
# --- 標籤查詢(資料來源:model-tags.tsv) ---
|
||
|
||
# 某階段的必要標籤;不限標籤的階段回傳 any。查不到不輸出。
|
||
stage_tags() { # $1=階段
|
||
[ -s "$TAGS_TSV" ] || return 0
|
||
awk -F'\t' -v s="$1" '$1 == "stage" && $2 == s { print $3; exit }' "$TAGS_TSV"
|
||
}
|
||
|
||
# 某模型的能力標籤。查法:先找完全相同的鍵,沒有才退回「表列鍵是實際 id 的前綴」
|
||
# 之中最長的一筆(例:表列 claude-haiku-4-5 對得上 claude-haiku-4-5-20251001)。
|
||
# 只認前綴這個方向。反向包含(實際 id 是表列鍵的前綴)會讓 gpt-5.x 命中更長的
|
||
# gpt-5.x-mini,最長鍵勝出就把 mini 的標籤發給 gpt-5.x,把夠格的模型擋掉。
|
||
model_tags() { # $1=模型 id
|
||
[ -s "$TAGS_TSV" ] || return 0
|
||
awk -F'\t' -v m="$1" '
|
||
$1 == "model" && $2 == m { exact = $3 }
|
||
$1 == "model" && $2 != m && substr(m, 1, length($2)) == $2 {
|
||
if (length($2) > best_len) { best_len = length($2); best = $3 }
|
||
}
|
||
END {
|
||
if (exact != "") print exact
|
||
else if (best != "") print best
|
||
}
|
||
' "$TAGS_TSV"
|
||
}
|
||
|
||
# 必要標籤中,該模型缺少的部分(逗號分隔);全部具備則不輸出。
|
||
missing_tags() { # $1=必要標籤 $2=模型標籤
|
||
awk -v req="$1" -v have="$2" '
|
||
BEGIN {
|
||
n = split(have, h, ","); for (i = 1; i <= n; i++) owned[h[i]] = 1
|
||
n = split(req, r, ",")
|
||
for (i = 1; i <= n; i++) if (!(r[i] in owned)) out = (out == "" ? r[i] : out "," r[i])
|
||
print out
|
||
}'
|
||
}
|
||
|
||
# 具備某組必要標籤的模型清單,用來告訴使用者可以切去哪個模型。
|
||
eligible_models() { # $1=必要標籤
|
||
[ -s "$TAGS_TSV" ] || return 0
|
||
awk -F'\t' -v req="$1" '
|
||
$1 == "model" {
|
||
split("", owned, ":")
|
||
n = split($3, h, ","); for (i = 1; i <= n; i++) owned[h[i]] = 1
|
||
n = split(req, r, ","); ok = 1
|
||
for (i = 1; i <= n; i++) if (!(r[i] in owned)) ok = 0
|
||
if (ok) out = (out == "" ? $2 : out "、" $2)
|
||
}
|
||
END { print out }
|
||
' "$TAGS_TSV"
|
||
}
|
||
|
||
model_info() {
|
||
current_model_report
|
||
}
|
||
|
||
model_verdict() { # $1=stage $2=req $3=model
|
||
stage="$1"; req="$2"; cur="$3"
|
||
[ -n "$cur" ] || { printf 'unknown-model'; return 0; }
|
||
[ "$req" = "any" ] && { printf 'pass'; return 0; }
|
||
have=$(model_tags "$cur")
|
||
[ -n "$have" ] || { printf 'unknown-model'; return 0; }
|
||
miss=$(missing_tags "$req" "$have")
|
||
[ -z "$miss" ] && printf 'pass' || printf 'fail:%s' "$miss"
|
||
}
|
||
|
||
model_fail_message() { # $1=stage $2=checked
|
||
echo "[jsc][SDLC 閘門][ERR]:判定不出目前實際使用的模型,無法驗證是否符合階段「$1」。已檢查來源:${2:-無}。請確認 hook stdin 帶有 transcript_path 或 model 欄位、Codex session 記錄可讀,或明確設定 JSC_MODEL 作為人工覆寫後重跑。本次不進行任何工作。" >&2
|
||
}
|
||
|
||
# --- 工作包 PR 閘門(狀態檔:$JSC_HOME/wp/{owner}-{repo}-{index}.pr,一個工作包一支) ---
|
||
#
|
||
# 刻意不綁 session:PR 沒合併就是沒合併,換一個工作階段照樣要擋。綁 session 等於給閘門
|
||
# 留一道「開新對話就自動繞過」的門,規則就不再是強制的。
|
||
#
|
||
# 一律只讀檔案,絕不打網路:hook 要快、也要能離線用。PR 的真實合併狀態由
|
||
# jsc-sdlc/tools/wp-gate.sh 去查並負責結清,本檔只反映已記錄的未結清狀態。
|
||
|
||
WP_DIR="$JSC_HOME/wp"
|
||
|
||
wp_state_file() { # $1={owner}/{repo} $2=index
|
||
printf '%s/%s-%s.pr' "$WP_DIR" "$(printf '%s' "$1" | tr '/' '-')" "$2"
|
||
}
|
||
|
||
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=$(wp_read_lock "$_f")
|
||
[ -n "$_line" ] && printf '%s\n' "$_line"
|
||
done
|
||
}
|
||
|
||
# 某個存取庫目前領取中的工作包代號;沒有領取檔或沒寫代號就不輸出(查無歸屬)。
|
||
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 }'
|
||
}
|
||
|
||
# 存取庫參數格式檢查;不合格就回 1,由呼叫端印訊息後 exit 2。
|
||
wp_valid_repo() { # $1=參數
|
||
case "${1:-}" in
|
||
*/*/*|/*|*/) return 1 ;;
|
||
*/*) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
case "${1:-check}" in
|
||
lock)
|
||
stage="${2:-}"
|
||
case " $STAGES " in
|
||
*" $stage "*) ;;
|
||
*) echo "[jsc][SDLC 閘門][ERR]:階段須為 $STAGES 之一,收到「${stage:-空值}」。" >&2; exit 1 ;;
|
||
esac
|
||
|
||
req=$(stage_tags "$stage")
|
||
if [ -z "$req" ]; then
|
||
echo "[jsc][SDLC 閘門][ERR]:讀不到階段「$stage」的必要標籤($TAGS_TSV 不存在或缺該階段)。請先執行 jsc-cli/tools/model-tags.sh sync 再重跑本階段,本次不進行任何工作。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
info=$(model_info)
|
||
cur=$(printf '%s' "$info" | cut -f1)
|
||
src=$(printf '%s' "$info" | cut -f2)
|
||
checked=$(printf '%s' "$info" | cut -f3-)
|
||
if [ -z "$cur" ]; then
|
||
model_fail_message "$stage" "$checked"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$req" != "any" ]; then
|
||
have=$(model_tags "$cur")
|
||
if [ -z "$have" ]; then
|
||
echo "[jsc][SDLC 閘門][ERR]:模型「$cur」不在能力標籤表上,無法判定是否夠格跑階段「$stage」(需要 $req)。請把該模型補進 jsc-cli/references/model-tags.md 後執行 model-tags.sh sync,本次不進行任何工作。" >&2
|
||
exit 1
|
||
fi
|
||
miss=$(missing_tags "$req" "$have")
|
||
if [ -n "$miss" ]; then
|
||
ok=$(eligible_models "$req")
|
||
echo "[jsc][SDLC 閘門][ERR]:階段「$stage」需要標籤「$req」,模型「$cur」缺少「$miss」。請切換到下列任一模型後重跑:${ok:-(表上無合格模型,請補表)}。本次不進行任何工作。" >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
printf '%s\t%s\t%s\t%s\n' "$stage" "$req" "$cur" "$src" > "$state" 2>/dev/null || {
|
||
echo "[jsc][SDLC 閘門][ERR]:寫不進狀態檔 $state,階段鎖未生效。" >&2; exit 1; }
|
||
echo "[jsc][SDLC 閘門][OK]:階段「$stage」通過(需要 $req,目前模型 $cur,來源 $src),已上鎖。"
|
||
exit 0 ;;
|
||
|
||
unlock)
|
||
rm -f "$state" 2>/dev/null || true
|
||
exit 0 ;;
|
||
|
||
check)
|
||
[ -f "$state" ] || exit 0
|
||
stage=$(cut -f1 "$state" 2>/dev/null | head -n1)
|
||
req=$(cut -f2 "$state" 2>/dev/null | head -n1)
|
||
# 舊格式(空白分隔、無必要標籤欄)讀不出 req,安靜降級不擋。
|
||
[ -n "$stage" ] && [ -n "$req" ] && [ "$stage" != "$req" ] || exit 0
|
||
[ "$req" = "any" ] && exit 0
|
||
|
||
info=$(model_info)
|
||
cur=$(printf '%s' "$info" | cut -f1)
|
||
src=$(printf '%s' "$info" | cut -f2)
|
||
checked=$(printf '%s' "$info" | cut -f3-)
|
||
# 判定不出模型時只提醒,不擋——否則使用者會被鎖在無法送出提示的狀態。
|
||
if [ -z "$cur" ]; then
|
||
echo "[jsc] SDLC 階段「${stage}」需要標籤「${req}」,但判定不出目前模型。已檢查來源:${checked:-無}。請確認 transcript、Codex session 記錄,或設定 JSC_MODEL 人工覆寫。"
|
||
exit 0
|
||
fi
|
||
|
||
have=$(model_tags "$cur")
|
||
if [ -z "$have" ]; then
|
||
echo "[jsc][SDLC 閘門][ERR]:模型「${cur}」(來源 ${src:-未知})不在能力標籤表上,無法確認是否夠格跑階段「${stage}」(需要 ${req})。請補進 jsc-cli/references/model-tags.md 並執行 model-tags.sh sync;確定要放行請執行 jsc-hooks/hooks/sdlc-gate.sh unlock。本輪提示已擋下。" >&2
|
||
exit 2
|
||
fi
|
||
|
||
miss=$(missing_tags "$req" "$have")
|
||
[ -z "$miss" ] && exit 0
|
||
|
||
ok=$(eligible_models "$req")
|
||
echo "[jsc][SDLC 閘門][ERR]:SDLC 階段「${stage}」需要標籤「${req}」,目前模型「${cur}」(來源 ${src:-未知})缺少「${miss}」。請切換到下列任一模型後重送:${ok:-(表上無合格模型,請補表)}。要結束本階段的鎖請執行 jsc-hooks/hooks/sdlc-gate.sh unlock。本輪提示已擋下。" >&2
|
||
exit 2 ;;
|
||
|
||
report)
|
||
if [ -f "$state" ]; then
|
||
stage=$(cut -f1 "$state" 2>/dev/null | head -n1)
|
||
req=$(cut -f2 "$state" 2>/dev/null | head -n1)
|
||
locked_model=$(cut -f3 "$state" 2>/dev/null | head -n1)
|
||
locked_source=$(cut -f4 "$state" 2>/dev/null | head -n1)
|
||
info=$(model_info)
|
||
cur=$(printf '%s' "$info" | cut -f1)
|
||
src=$(printf '%s' "$info" | cut -f2)
|
||
[ -n "$cur" ] || cur="$locked_model"
|
||
[ -n "$src" ] || src="$locked_source"
|
||
verdict=$(model_verdict "$stage" "$req" "$cur")
|
||
[ -n "$stage" ] && echo "$sid $stage $req ${cur:-未知} ${src:-未知} $verdict"
|
||
fi
|
||
exit 0 ;;
|
||
|
||
wp-lock)
|
||
repo="${2:-}"; idx="${3:-}"; wp="${4:-}"
|
||
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
|
||
mkdir -p "$WP_DIR" 2>/dev/null || true
|
||
wpf=$(wp_state_file "$repo" "$idx")
|
||
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 未結清${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)
|
||
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" "$idx")" 2>/dev/null || true
|
||
exit 0 ;;
|
||
|
||
wp-report)
|
||
wp_pending
|
||
exit 0 ;;
|
||
|
||
wp-check)
|
||
[ "${JSC_WP_GATE:-}" = "off" ] && exit 0
|
||
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 <<WP_PENDING_EOF
|
||
$pending
|
||
WP_PENDING_EOF
|
||
|
||
case "${2:-prompt}" in
|
||
prompt)
|
||
# 只注入提醒,一律 exit 0。擋提示會連「去把那支 PR 修好」的對話都送不出去,
|
||
# 把使用者鎖在門外,連逃生門都下不了指令。
|
||
# 這裡只列得出「哪些工作包還沒結清」,不知道候選包相依於誰——沒有上下文可以判斷。
|
||
# 真正「這一包能不能挑」的判斷在 jsc-sdlc/tools/wp-gate.sh check-deps,這則只是提醒。
|
||
echo "[jsc] ${brief} PR 尚未合併。相依於它的工作包不能開始,其餘互不相依的工作包不受影響——是否可挑,見 wp-gate.sh check-deps。"
|
||
[ -n "$foreign" ] && echo "[jsc] 其中 ${foreign}不屬於這個存取庫領取中的工作包。那幾支交給領取它的工作階段結清:這裡不要改它的程式碼,也不要回它的留言。"
|
||
exit 0 ;;
|
||
skill)
|
||
# 技能名取法比照 version-guard.sh:環境變數優先,非 Claude CLI 只餵得到環境變數。
|
||
skill="${JSC_SKILL:-${SKILL:-$(json_str skill)}}"
|
||
# 取不到技能名就安靜降級放行,不能拿沒有的資料當擋人的理由。
|
||
[ -n "$skill" ] || exit 0
|
||
# 帶前綴(jsc-sdlc:plan)與裸名(plan)都要認。
|
||
sname=${skill##*:}
|
||
case "$sname" in
|
||
analyze|maintain)
|
||
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 ;;
|
||
plan)
|
||
# plan 只提醒不擋,理由見檔頭「plan 已從擋人名單移出」。放棄的是在製品上限,
|
||
# 換來的是不再誤擋跟那支 PR 無關的新計畫;analyze 與 maintain 兩道仍在。
|
||
echo "[jsc][工作包閘門]:${brief} PR 尚未合併。plan 是純邏輯階段、不碰程式碼,這裡只提醒不擋,但新計畫排進實作前請先把它結清。"
|
||
[ -n "$foreign" ] && echo "[jsc][工作包閘門]:${foreign}不屬於這個存取庫領取中的工作包。那幾支交給領取它的工作階段結清:這裡不要改它的程式碼,也不要回它的留言。"
|
||
exit 0 ;;
|
||
implement)
|
||
# implement 一律放行,連別包的 PR 未結清也放行:結清 PR 正是 implement 步驟 2 要做
|
||
# 的事,擋掉就沒有任何路徑能解除這道鎖。領取檔不綁工作階段,擋下去連領取那一包的
|
||
# 工作階段都會被自己的舊紀錄擋住,等於把流程鎖死。所以這裡只注入歸屬提醒。
|
||
[ -n "$foreign" ] && echo "[jsc][工作包閘門]:${foreign}不屬於這個存取庫領取中的工作包。這裡只結清自己領取那一包的 PR${mine:+(${mine})};別包的 PR 不要改、留言也不要回,交給領取它的工作階段。"
|
||
exit 0 ;;
|
||
esac
|
||
# 其餘技能一律放行:這道閘門管的是 SDLC 階段,不是整台機器的技能呼叫。
|
||
exit 0 ;;
|
||
esac
|
||
exit 0 ;;
|
||
esac
|
||
exit 0
|