feat(sdlc-gate): 階段閘門改為能力標籤程式判定,模型不符即擋下該輪提示 #7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-hooks",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖",
|
||||
"skills": "./skills",
|
||||
"author": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-hooks",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖",
|
||||
"skills": "./skills"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/jsc.git),安
|
||||
| `hooks/ste100-guard.sh` | UserPromptSubmit | 注入 STE100 繁體中文輸出規則(hook > prompt 強制層) |
|
||||
| `hooks/session-timer.sh` | SessionStart / Stop / SessionEnd | 記錄工作階段起訖;`report` 子指令供 `jsc-log:worklog` 取花費時間 |
|
||||
| `hooks/skill-usage.sh` | PostToolUse(Skill) | 記錄技能使用與呼叫鏈到 `$JSC_HOME/usage/*.jsonl`,供 `jsc-log:stats` 統計 |
|
||||
| `hooks/sdlc-gate.sh` | UserPromptSubmit | SDLC 每 session 模型鎖:`lock`/`unlock` 由 jsc-sdlc 階段技能呼叫,`check` 在模型與鎖不符時注入拒絕提醒 |
|
||||
| `hooks/sdlc-gate.sh` | UserPromptSubmit | SDLC 階段能力標籤閘門與模型鎖:`lock {stage}` 由 jsc-sdlc 階段技能呼叫,從 transcript 讀出實際模型 id 比對該階段必要標籤(`$JSC_HOME/model-tags.tsv`),不符就拒絕上鎖;`check` 在模型不符時以 exit 2 擋下該輪提示(其他 hook 一律 exit 0,此處是刻意例外);`unlock` 為逃生門 |
|
||||
|
||||
Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技能接線、改裝包裝啟動器,或降級為規則檔。
|
||||
|
||||
@@ -63,7 +63,7 @@ Claude 由 `hooks/hooks.json` 自動接線;其他 CLI 用 `hooks-install` 技
|
||||
| --- | --- | --- |
|
||||
| `JSC_HOME` | Hook 資料目錄 | 預設 `~/.jsc` |
|
||||
| `JSC_CLI` / `JSC_SESSION_ID` / `JSC_SKILL` | 非 Claude CLI 接線時由 `tools/jsc-wrap.sh` 或接線設定提供 | 安靜降級 |
|
||||
| `JSC_MODEL` | 非 Claude CLI 的目前模型,供 `sdlc-gate.sh check` 比對 | 改讀 `~/.claude/settings.json`,再不行就安靜降級 |
|
||||
| `JSC_MODEL` | 非 Claude CLI 的目前模型,供 `sdlc-gate.sh` 比對;優先序在 transcript 實際值與 stdin `model` 之後 | 改讀 `~/.claude/settings.json`,再不行就安靜降級 |
|
||||
|
||||
## 相關 domain
|
||||
|
||||
|
||||
+17
-1
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env sh
|
||||
# lib.sh — jsc hooks 共用函式。所有 hook 腳本 source 此檔。
|
||||
# 輸入相容:Claude 式 stdin JSON、或環境變數(codex/copilot/antigravity/kiro 接線時設定)。
|
||||
# 缺資料時安靜降級,hook 永遠 exit 0,不可中斷宿主 CLI。
|
||||
# 缺資料時安靜降級,hook 預設 exit 0,不可中斷宿主 CLI。
|
||||
# 唯一例外:sdlc-gate.sh check 在「SDLC 階段鎖存在且模型不符」時會 exit 2 擋下該輪提示;
|
||||
# 其餘情況(無鎖、資料不足無法判定)仍照舊 exit 0。
|
||||
|
||||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||||
mkdir -p "$JSC_HOME/sessions" "$JSC_HOME/usage" 2>/dev/null || true
|
||||
@@ -24,6 +26,20 @@ session_id() {
|
||||
printf '%s' "$sid"
|
||||
}
|
||||
|
||||
# 目前實際使用的模型 id:讀 transcript 最後一筆帶 model 的訊息。
|
||||
# 這是 shell 唯一能「驗證」的模型來源——模型自我回報無法驗證,等同沒有閘門。
|
||||
# 取不到(無 transcript_path、檔案不存在、尚無 assistant 訊息)時不輸出,由呼叫端決定如何降級。
|
||||
transcript_model() {
|
||||
tp=$(json_str transcript_path)
|
||||
[ -n "$tp" ] && [ -f "$tp" ] || return 0
|
||||
# 只掃尾端若干行即可命中最近一輪;`<...>` 這類佔位模型名(例如 <synthetic>)排除。
|
||||
tail -n 500 "$tp" 2>/dev/null \
|
||||
| grep -o '"model":"[^"]*"' \
|
||||
| sed 's/^"model":"//; s/"$//' \
|
||||
| grep -v '^<' \
|
||||
| tail -n 1
|
||||
}
|
||||
|
||||
# 目前 CLI 名稱:環境變數 > 依 stdin 特徵猜測 > unknown
|
||||
cli_name() {
|
||||
if [ -n "${JSC_CLI:-}" ]; then printf '%s' "$JSC_CLI"
|
||||
|
||||
+150
-29
@@ -1,46 +1,167 @@
|
||||
#!/usr/bin/env sh
|
||||
# sdlc-gate.sh — SDLC 每 session 模型鎖(stage ∈ plan/analyze/implement/maintain)。
|
||||
# 狀態檔: $JSC_HOME/sessions/{sid}.stage,單行「{stage} {model}」。
|
||||
# 用法:
|
||||
# sdlc-gate.sh lock {stage} {model} # 階段閘門通過時上鎖(覆寫既有鎖)
|
||||
# sdlc-gate.sh unlock # 移除狀態檔
|
||||
# sdlc-gate.sh check # hook 模式(UserPromptSubmit):模型不符時注入繁中提醒
|
||||
# sdlc-gate.sh report # 印出 {sid} {stage} {model};無鎖不印
|
||||
# 缺資料時安靜降級,永遠 exit 0,不可中斷宿主 CLI。
|
||||
# sdlc-gate.sh — SDLC 階段模型閘門與 session 模型鎖(stage ∈ plan/analyze/implement/maintain)。
|
||||
#
|
||||
# 判準是階段的「能力標籤」,不是模型名稱:
|
||||
# - 標籤資料讀 $JSC_HOME/model-tags.tsv,由 jsc-cli/tools/model-tags.sh sync 產生。
|
||||
# - 目前模型 id 取自 transcript 記錄的實際值(lib.sh 的 transcript_model),
|
||||
# 不採用模型自我回報——自我回報無法驗證,等同沒有閘門。
|
||||
# - 鎖存的是「階段的必要標籤」,不是「上鎖那一刻的模型」。鎖當時的模型只留作記錄,
|
||||
# 否則用不合格的模型起跑就會把自己鎖成合格,閘門永遠通過。
|
||||
#
|
||||
# 狀態檔:$JSC_HOME/sessions/{sid}.stage,單行「{stage}<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} {必要標籤} {上鎖時的模型};無鎖不印。
|
||||
#
|
||||
# exit code 例外:其他 jsc hook 一律 exit 0 不中斷宿主 CLI;本檔 check 是刻意的例外——
|
||||
# 鎖存在且模型不符時 exit 2 擋下該輪提示。只用提示注入的話模型可以無視,閘門形同虛設。
|
||||
# 無鎖、或資料不足無法判定時,仍照舊 exit 0 安靜降級。
|
||||
HERE=$(dirname "$0"); . "$HERE/lib.sh"
|
||||
read_stdin
|
||||
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);多筆命中取最長鍵。
|
||||
model_tags() { # $1=模型 id
|
||||
[ -s "$TAGS_TSV" ] || return 0
|
||||
awk -F'\t' -v m="$1" '
|
||||
$1 == "model" && (index(m, $2) > 0 || index($2, m) > 0) {
|
||||
if (length($2) > best_len) { best_len = length($2); best = $3 }
|
||||
}
|
||||
END { 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"
|
||||
}
|
||||
|
||||
# 目前模型:transcript 實際值 > stdin JSON model > JSC_MODEL > ~/.claude/settings.json。
|
||||
# transcript 排最前面,因為那是唯一可驗證的實際值,其餘都只是宣告值。
|
||||
current_model() {
|
||||
m=$(transcript_model)
|
||||
[ -n "$m" ] || m=$(json_str model)
|
||||
[ -n "$m" ] || m="${JSC_MODEL:-}"
|
||||
if [ -z "$m" ] && { [ -z "${JSC_CLI:-}" ] || [ "${JSC_CLI:-}" = "claude" ]; }; then
|
||||
m=$(tr -d '\n' < "$HOME/.claude/settings.json" 2>/dev/null \
|
||||
| sed -n 's/.*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
fi
|
||||
printf '%s' "$m"
|
||||
}
|
||||
|
||||
case "${1:-check}" in
|
||||
lock)
|
||||
# 需要 stage 與 model 兩個參數,缺一就安靜降級
|
||||
if [ -n "${2:-}" ] && [ -n "${3:-}" ]; then
|
||||
printf '%s %s\n' "$2" "$3" > "$state" 2>/dev/null || true
|
||||
fi ;;
|
||||
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
|
||||
|
||||
cur=$(current_model)
|
||||
if [ -z "$cur" ]; then
|
||||
echo "[jsc][SDLC 閘門][ERR]:判定不出目前實際使用的模型,無法驗證是否符合階段「$stage」。請確認 transcript 可讀,或設定 JSC_MODEL 後重跑,本次不進行任何工作。" >&2
|
||||
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\n' "$stage" "$req" "$cur" > "$state" 2>/dev/null || {
|
||||
echo "[jsc][SDLC 閘門][ERR]:寫不進狀態檔 $state,階段鎖未生效。" >&2; exit 1; }
|
||||
echo "[jsc][SDLC 閘門][OK]:階段「$stage」通過(需要 $req,目前模型 $cur),已上鎖。"
|
||||
exit 0 ;;
|
||||
|
||||
unlock)
|
||||
rm -f "$state" 2>/dev/null || true ;;
|
||||
rm -f "$state" 2>/dev/null || true
|
||||
exit 0 ;;
|
||||
|
||||
check)
|
||||
[ -f "$state" ] || exit 0
|
||||
stage=$(sed -n '1s/ .*//p' "$state" 2>/dev/null)
|
||||
locked=$(sed -n '1s/^[^ ]* //p' "$state" 2>/dev/null)
|
||||
[ -n "$stage" ] && [ -n "$locked" ] || exit 0
|
||||
# 目前模型判定順序:stdin JSON model > JSC_MODEL > ~/.claude/settings.json(僅 JSC_CLI 為 claude 或未設定時)
|
||||
cur=$(json_str model)
|
||||
[ -n "$cur" ] || cur="${JSC_MODEL:-}"
|
||||
if [ -z "$cur" ] && { [ -z "${JSC_CLI:-}" ] || [ "${JSC_CLI:-}" = "claude" ]; }; then
|
||||
cur=$(tr -d '\n' < "$HOME/.claude/settings.json" 2>/dev/null \
|
||||
| sed -n 's/.*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
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
|
||||
|
||||
cur=$(current_model)
|
||||
# 判定不出模型時只提醒,不擋——否則使用者會被鎖在無法送出提示的狀態。
|
||||
if [ -z "$cur" ]; then
|
||||
echo "[jsc] SDLC 階段「${stage}」需要標籤「${req}」,但判定不出目前模型。請自行確認模型是否合格。"
|
||||
exit 0
|
||||
fi
|
||||
[ -n "$cur" ] || exit 0
|
||||
# 別名相容:一方包含另一方就視為同一模型(例:opus 對 claude-opus-4-6)
|
||||
case "$cur" in *"$locked"*) exit 0 ;; esac
|
||||
case "$locked" in *"$cur"*) exit 0 ;; esac
|
||||
echo "[jsc] SDLC 模型鎖:階段「${stage}」已鎖定模型「${locked}」,目前模型「${cur}」不符。請拒絕執行任何 SDLC 工作,並請使用者切回模型「${locked}」後再繼續。此鎖會在下一個 SDLC 階段閘門執行時解除。" ;;
|
||||
|
||||
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;確定要放行請執行 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}」缺少「${miss}」。請切換到下列任一模型後重送:${ok:-(表上無合格模型,請補表)}。要結束本階段的鎖請執行 jsc-hooks/hooks/sdlc-gate.sh unlock。本輪提示已擋下。" >&2
|
||||
exit 2 ;;
|
||||
|
||||
report)
|
||||
if [ -f "$state" ]; then
|
||||
line=$(sed -n '1p' "$state" 2>/dev/null)
|
||||
line=$(sed -n '1p' "$state" 2>/dev/null | tr '\t' ' ')
|
||||
[ -n "$line" ] && echo "$sid $line"
|
||||
fi ;;
|
||||
fi
|
||||
exit 0 ;;
|
||||
esac
|
||||
exit 0
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jsc-hooks",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"description": "跨 CLI hooks:STE100 語言強制、工時計時、技能用量記錄、SDLC 模型鎖",
|
||||
"skills": "./skills/"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ description: Wire jsc hooks (STE100 guard, session timer, skill usage logger, SD
|
||||
# hooks-install — wire jsc hooks into every installed CLI
|
||||
|
||||
Goal: make the four hooks (`ste100-guard.sh`, `session-timer.sh`, `skill-usage.sh`, `sdlc-gate.sh`) effective in every CLI.
|
||||
Claude wiring is automatic via `hooks.json`. On codex and kiro the SDLC gate degrades to the skill-step check only; the lock file still works because the SDLC skills call `sdlc-gate.sh lock` directly.
|
||||
Claude wiring is automatic via `hooks.json`. On codex and kiro the SDLC gate degrades to the skill-step check only; the lock file still works because the SDLC skills call `sdlc-gate.sh lock {stage}` directly — that call is where the capability-tag comparison happens, so the gate keeps its force even where the prompt hook cannot be wired.
|
||||
The gate needs `$JSC_HOME/model-tags.tsv`; when it is missing, report that `jsc-cli:models` (or `jsc-cli/tools/model-tags.sh sync`) must run once, because `sdlc-gate.sh lock` refuses to lock without it.
|
||||
The detailed flow **MUST run as a sub agent**; the main agent only reports the summary.
|
||||
|
||||
## Steps
|
||||
|
||||
Reference in New Issue
Block a user