fix(sdlc-gate): 能力閘門依 CLI 分流,判不出能力就擋下
What:
- current_model_report() 的偵測鏈依 CLI 分流,一支只讀自己的紀錄。claude 讀 transcript 與 hook stdin,codex 讀 hook stdin 與自己的 session 記錄,copilot、antigravity、kiro 本機沒有可讀的模型紀錄,判不出是哪一支 CLI 時不採用任何自動來源。JSC_MODEL 人工覆寫五支都保留,而且一律排在最後。
- session_id() 的退路鏈補上 CLAUDE_CODE_SESSION_ID,cli_name() 補上 CLAUDECODE 與 CLAUDE_CODE_SESSION_ID。
- 階段鎖狀態檔改成 sessions/{CLI 代號}-{sid}.stage。舊路徑仍讀得到,unlock 不帶參數時新舊兩份一起清,也收一個狀態檔路徑當參數。
- check 改成 fail-closed:判不出 CLI、判不出模型、模型不在能力標籤表上,三種一律以 exit 2 擋下該輪提示。每一則擋下的訊息都印出兩條逃生門。
- write-guard.sh 的 stage 模式改讀 lib.sh 的路徑函式,擋人訊息把實際讀到的狀態檔路徑寫進解除指令。
- codex_session_file() 取「全樹最新一支」的做法改掉。
- wire-cli.sh 的冒煙夾具跟著補:模型來源四條各自指定 CLI 代號,另加三種擋下情形、check 的 fail-closed、階段鎖的 CLI 區隔與舊格式相容,這一組的判定條數由四條增為十二條。
- README 的 hooks 表、狀態檔分界表與環境變數表跟著改。
Why:
- 使用者回報兩件事:在 claude 底下被 codex 的模型判定,而且這道閘門應該只鎖能力標籤、不鎖模型。查下來根因是同一個——閘門完全不分辨自己跑在哪一支 CLI 上。
- 偵測鏈不分流,claude 拿不到 transcript 就一路掉進 codex 的 session 記錄,拿別支的模型判這一支。順帶每一輪都對一個上千個檔、兩百多 MB 的目錄樹跑一次 find,宿主 CLI 跟著卡。
- session_id() 讀的變數名在目前的 claude 上並不存在,sid 一路退回 default。cli_name() 犯同一類錯:只認 plugin 接線才有的變數,技能以 Bash 工具呼叫腳本時取不到,一律判成 unknown。
- 前三項合起來的後果是:技能呼叫 lock 算出的檔名,跟 hook 算出的檔名永遠不同。階段鎖上了也對不上,所以這道閘門在 claude 上一直沒有真正生效。
- 狀態檔不分 CLI,各支就共用同一支 default.stage,一支上的鎖擋到另一支。那不只擋提示,write-guard.sh 的 stage 模式連 Write、Edit、MultiEdit 一起擋。
How:
- 政策改成 fail-closed:不知道能力就擋下,知道才比對標籤。原本判不出模型只提醒不擋,那等於「換一支讀不到紀錄的 CLI 就能繞過去」,閘門形同虛設。代價是把人鎖在送不出提示的狀態,所以每一則擋下的訊息都要帶兩條逃生門,照抄就脫困。
- 逃生門的路徑照抄進指令裡。擋人的是 hook,解鎖的是人在殼層手動執行,兩邊算出來的檔名未必相同,不點名就解不到真正擋人的那一支。
- unlock 的參數只收 sessions 目錄底下的 .stage 檔,逃生門不該順便變成任意刪檔的工具。判準刻意不綁這支行程算出的 JSC_HOME:擋人的一側跟解鎖的一側未必相同,綁上去就會把照抄訊息的人擋掉,那正是這個逃生門要避免的事。
- 狀態檔用檔名前綴不用子目錄。外部工具以單層的 sessions/*.stage 盤點階段鎖,改成子目錄會讓每一支鎖從那些盤點裡整批消失。
- 舊路徑只讀不搬也不刪,unlock 一併清掉。只清新的那一份,舊格式的鎖就永遠解不開,被它擋住的人沒有逃生門。
- 變數名只補實測看得到的那幾個,而且只補 claude 這一支。其他 CLI 的變數名與環境特徵沒有實測過,猜一個填進來只會多一個錯誤來源,那幾支本來就由接線設定明確帶 JSC_CLI。
- codex_session_file() 原本寫成 xargs ls -t 再 head -n 1,那是錯的:xargs 依參數長度分批,ls -t 只在自己那一批裡排序,取到的是第一批裡最新的,不是全域最新。改成讓 find 一併印出修改時間再全域排序;沒有 -printf 就退回路徑排序,那些檔名以 ISO 時間開頭,字典序等同時間序。
- 冒煙的擋人案例連訊息一起驗,不只比結束碼。訊息漏掉逃生門一樣是綠燈,而那才是這道 fail-closed 真正的風險。
- 四支腳本併成一筆。write-guard.sh 讀 lib.sh 的路徑函式,wire-cli.sh 是驗這些行為的冒煙夾具,拆開會留下跑不過的中間版本:路徑函式與呼叫端分兩筆,中間那一筆狀態檔的讀寫兩側就對不上。
- 一次性代價:升級後每台機器第一次 SessionStart 會清一次重啟閘門。sid 換了名字,看起來像新的工作階段。只發生這一次。
- 三份 manifest 這一筆不動,版本號另外提升。
Who:
SDLC 階段能力閘門。修的是它一直沒有生效的那條路徑,順帶把 codex session 記錄的取檔方式一起修掉。
This commit is contained in:
+117
-25
@@ -2,8 +2,9 @@
|
||||
# lib.sh — jsc hooks 共用函式。所有 hook 腳本 source 此檔。
|
||||
# 輸入相容:Claude 式 stdin JSON、或環境變數(codex/copilot/antigravity/kiro 接線時設定)。
|
||||
# 缺資料時安靜降級,hook 預設 exit 0,不可中斷宿主 CLI。
|
||||
# 唯一例外:sdlc-gate.sh check 在「SDLC 階段鎖存在且模型不符」時會 exit 2 擋下該輪提示;
|
||||
# 其餘情況(無鎖、資料不足無法判定)仍照舊 exit 0。
|
||||
# 唯一例外:sdlc-gate.sh check 在「SDLC 階段鎖存在,而且不知道目前模型的能力」時會 exit 2
|
||||
# 擋下該輪提示。不知道能力有三種:判不出是哪一支 CLI、判不出模型、模型不在能力標籤表上。
|
||||
# 這是刻意的 fail-closed——放行等於閘門不存在。沒有階段鎖時仍照舊 exit 0。
|
||||
#
|
||||
# 結束碼:不適用。本檔是被 source 的共用函式庫,不是可執行入口,內部一次 exit 都沒有。
|
||||
# 載入成功回 0(最後一行是函式定義);拿 `sh lib.sh` 直接跑也只是定義完函式回 0,不做事。
|
||||
@@ -31,12 +32,58 @@ json_str() { # $1=欄位名
|
||||
}
|
||||
|
||||
# 目前 session id:stdin JSON > 環境變數 > 固定值
|
||||
#
|
||||
# 環境變數這一段的順序:
|
||||
# JSC_SESSION_ID jsc 自己的指定值(tools/jsc-wrap.sh 會設),人工指定優先於偵測。
|
||||
# CLAUDE_CODE_SESSION_ID claude 實際匯出的名字,實測確認過。
|
||||
# CLAUDE_SESSION_ID 只留作往後相容,排在實測名之後。這個名字在目前的 claude 上並不
|
||||
# 存在,兩個都設到的話,該信的是實測看得到的那一個;只設到這一個
|
||||
# 的版本仍然退得下來,所以留著不會有損失。
|
||||
# 為什麼這一條要修:退路鏈原本只找 CLAUDE_SESSION_ID,那個名字取不到值,於是沒有 stdin 的
|
||||
# 執行路徑(技能以 Bash 呼叫 sdlc-gate.sh lock)一律退回 default,而 hook 有 stdin、拿得到
|
||||
# 真正的 id。同一個工作階段的兩條路徑因此算出兩支不同的狀態檔,階段鎖上了也永遠對不上。
|
||||
# 這裡只補退路鏈的變數名,不動取值順序以外的行為:其他 CLI 的變數名沒有實測過,猜一個填進來
|
||||
# 只會多一個錯誤來源。
|
||||
session_id() {
|
||||
sid=$(json_str session_id)
|
||||
[ -n "$sid" ] || sid="${JSC_SESSION_ID:-${CLAUDE_SESSION_ID:-default}}"
|
||||
[ -n "$sid" ] || sid="${JSC_SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-${CLAUDE_SESSION_ID:-default}}}"
|
||||
printf '%s' "$sid"
|
||||
}
|
||||
|
||||
# 階段鎖狀態檔名要用的 CLI 代號。這個值直接拼進檔名,所以不像代號的字元一律換掉;
|
||||
# 帶斜線或空白的值會把檔案寫到別的地方去,理由與 restart-gate.sh 的做法相同。
|
||||
stage_state_cli() {
|
||||
cli_name | sed 's/[^A-Za-z0-9._-]/_/g'
|
||||
}
|
||||
|
||||
# 階段鎖狀態檔的舊路徑:$JSC_HOME/sessions/{sid}.stage。只作往後相容的讀取來源,
|
||||
# 以及 unlock 的清除對象,新的寫入一律不走這裡。
|
||||
stage_state_legacy_file() {
|
||||
printf '%s/sessions/%s.stage' "$JSC_HOME" "$(session_id)"
|
||||
}
|
||||
|
||||
# 階段鎖狀態檔(寫入用):$JSC_HOME/sessions/{cli}-{sid}.stage。
|
||||
# 為什麼要帶 CLI 代號:session_id() 判不出工作階段時會退回 default,各 CLI 於是共用同一支
|
||||
# default.stage,一支上的階段鎖就會擋到另一支。那不只擋提示——write-guard.sh 的 stage 模式
|
||||
# 在 plan 或 analyze 持鎖時連 Write、Edit、MultiEdit 一起擋掉。
|
||||
# 為什麼用檔名前綴而不是子目錄:外部工具以單層的 sessions/*.stage 盤點階段鎖,改成子目錄
|
||||
# 會讓每一支鎖從那些盤點裡整批消失;前綴照樣列得到,只是多一段代號。
|
||||
stage_state_file() {
|
||||
printf '%s/sessions/%s-%s.stage' "$JSC_HOME" "$(stage_state_cli)" "$(session_id)"
|
||||
}
|
||||
|
||||
# 階段鎖狀態檔(讀取用):新路徑優先,沒有才回舊路徑。舊檔記的是實際工作,只讀不搬也不刪。
|
||||
# 兩邊都沒有時回新路徑,讓呼叫端一律用「檔案在不在」判斷有沒有鎖。
|
||||
stage_state_read_file() {
|
||||
_sf=$(stage_state_file)
|
||||
if [ -f "$_sf" ]; then
|
||||
printf '%s' "$_sf"
|
||||
else
|
||||
_so=$(stage_state_legacy_file)
|
||||
if [ -f "$_so" ]; then printf '%s' "$_so"; else printf '%s' "$_sf"; fi
|
||||
fi
|
||||
}
|
||||
|
||||
MODEL_SOURCE_CHECKS=""
|
||||
|
||||
model_checked() {
|
||||
@@ -75,9 +122,17 @@ codex_session_file() { # $1=CODEX_HOME $2=session id
|
||||
find "$ch/sessions" -type f -name "*$sid*.jsonl" 2>/dev/null | sort | tail -n 1
|
||||
return 0
|
||||
fi
|
||||
files=$(find "$ch/sessions" -type f -name '*.jsonl' 2>/dev/null)
|
||||
[ -n "$files" ] || return 1
|
||||
printf '%s\n' "$files" | xargs ls -t 2>/dev/null | head -n 1
|
||||
# 全樹最新的一支。原本寫成 `xargs ls -t | head -n 1`,那是錯的:xargs 會依參數長度分批,
|
||||
# ls -t 只在自己那一批裡排序,取到的是「第一批裡最新的」而不是全域最新。改成讓 find 一併
|
||||
# 印出修改時間,所有檔案在同一輪比較,批次邊界就影響不到結果。
|
||||
newest=$(find "$ch/sessions" -type f -name '*.jsonl' -printf '%T@\t%p\n' 2>/dev/null \
|
||||
| sort -rn | head -n 1 | cut -f2-)
|
||||
# find 沒有 -printf(非 GNU)時退回路徑排序。codex 的 session 依「年/月/日」分目錄,
|
||||
# 檔名又以 ISO 時間開頭,字典序等同時間序,仍然是全域比較,不會被分批切斷。
|
||||
[ -n "$newest" ] || newest=$(find "$ch/sessions" -type f -name '*.jsonl' 2>/dev/null \
|
||||
| sort | tail -n 1)
|
||||
[ -n "$newest" ] || return 1
|
||||
printf '%s\n' "$newest"
|
||||
}
|
||||
|
||||
codex_model() {
|
||||
@@ -98,28 +153,57 @@ codex_model() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# 目前模型的判定。輸出單行「{模型 id}<TAB>{來源}<TAB>{已檢查來源}」,判不出時前兩欄留空。
|
||||
#
|
||||
# 偵測鏈依 CLI 分流:一支 CLI 只讀自己的紀錄。兩個理由。
|
||||
# 一是正確性:跨過去讀別支的紀錄,拿到的是別支的模型,用它判定這一支等於沒有判準。
|
||||
# 二是速度:別支的 session 目錄可能有上千個檔案,每一輪 hook 都掃一次會把宿主 CLI 拖住。
|
||||
# 判不出是哪一支 CLI 時不猜任何來源——不知道是誰,就不知道該讀誰的紀錄。
|
||||
# JSC_MODEL 這個人工覆寫對所有 CLI 都保留,而且一律排在最後:可驗證的紀錄優先於人工宣告。
|
||||
current_model_report() {
|
||||
MODEL_SOURCE_CHECKS=""
|
||||
_mcli=$(cli_name)
|
||||
model_checked "JSC_CLI:$_mcli"
|
||||
|
||||
tp=$(json_str transcript_path)
|
||||
[ -n "$tp" ] || tp="${JSC_TRANSCRIPT_PATH:-}"
|
||||
model_checked "transcript_path:${tp:-未提供}"
|
||||
if [ -n "$tp" ] && [ -f "$tp" ]; then
|
||||
m=$(transcript_model)
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "transcript:$tp" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
fi
|
||||
case "$_mcli" in
|
||||
claude)
|
||||
# transcript 是 claude 自己寫下的執行紀錄,也是唯一逐輪更新的來源。
|
||||
tp=$(json_str transcript_path)
|
||||
[ -n "$tp" ] || tp="${JSC_TRANSCRIPT_PATH:-}"
|
||||
model_checked "transcript_path:${tp:-未提供}"
|
||||
if [ -n "$tp" ] && [ -f "$tp" ]; then
|
||||
m=$(transcript_model)
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "transcript:$tp" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
fi
|
||||
|
||||
model_checked "hook-stdin:model/model_id/model_slug/modelName/current_model/currentModel"
|
||||
m=$(json_model_value "$STDIN_JSON")
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "hook-stdin" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
model_checked "hook-stdin:model/model_id/model_slug/modelName/current_model/currentModel"
|
||||
m=$(json_model_value "$STDIN_JSON")
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "hook-stdin" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
;;
|
||||
codex)
|
||||
# hook 負載是這一輪由 codex 自己餵進來的,不是別支 CLI 的紀錄,所以照收;
|
||||
# 而且它反映當下這一輪,比落在檔案裡的紀錄新,排在 session 記錄前面。
|
||||
model_checked "hook-stdin:model/model_id/model_slug/modelName/current_model/currentModel"
|
||||
m=$(json_model_value "$STDIN_JSON")
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "hook-stdin" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
|
||||
model_checked "codex:${CODEX_HOME:-$HOME/.codex}、~/.codex/sessions、history.jsonl、session_index.jsonl"
|
||||
cm=$(codex_model 2>/dev/null || true)
|
||||
if [ -n "$cm" ]; then
|
||||
m=$(printf '%s' "$cm" | cut -f1)
|
||||
src=$(printf '%s' "$cm" | cut -f2)
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "$src" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
fi
|
||||
model_checked "codex:${CODEX_HOME:-$HOME/.codex}/sessions、history.jsonl、session_index.jsonl"
|
||||
cm=$(codex_model 2>/dev/null || true)
|
||||
if [ -n "$cm" ]; then
|
||||
m=$(printf '%s' "$cm" | cut -f1)
|
||||
src=$(printf '%s' "$cm" | cut -f2)
|
||||
[ -n "$m" ] && { printf '%s\t%s\t%s\n' "$(model_clean "$m")" "$src" "$MODEL_SOURCE_CHECKS"; return 0; }
|
||||
fi
|
||||
;;
|
||||
copilot|antigravity|kiro)
|
||||
# 這三支由 tools/jsc-wrap.sh 包起來跑,只餵得到環境變數:沒有 transcript、沒有 hook
|
||||
# 負載,本機也沒有可讀的模型紀錄。這裡如實記成「沒有來源」,不去翻別支 CLI 的檔案。
|
||||
model_checked "$_mcli:本機沒有可讀的模型紀錄,只認 JSC_MODEL"
|
||||
;;
|
||||
*)
|
||||
model_checked "未知 CLI:判不出是哪一支,不採用任何自動來源"
|
||||
;;
|
||||
esac
|
||||
|
||||
model_checked "JSC_MODEL"
|
||||
if [ -n "${JSC_MODEL:-}" ]; then
|
||||
@@ -130,10 +214,18 @@ current_model_report() {
|
||||
printf '\t\t%s\n' "$MODEL_SOURCE_CHECKS"
|
||||
}
|
||||
|
||||
# 目前 CLI 名稱:環境變數 > 依 stdin 特徵猜測 > unknown
|
||||
# 目前 CLI 名稱:環境變數 > 依環境特徵判斷 > unknown
|
||||
#
|
||||
# claude 的判準有三個,任一個成立就算。理由與 session_id() 那條退路鏈同源:CLAUDE_PLUGIN_ROOT
|
||||
# 只有 plugin 接線的 hook 執行環境才有,技能以 Bash 工具呼叫腳本時並不存在,於是同一個工作
|
||||
# 階段的兩條路徑一條認得出 claude、一條回 unknown,狀態檔名跟著分岔。CLAUDECODE 與
|
||||
# CLAUDE_CODE_SESSION_ID 兩者在工具呼叫的環境裡都看得到(實測確認),補上去兩條路徑才算得出
|
||||
# 同一個代號。只補 claude 這一支:其他 CLI 的環境特徵沒有實測過,猜一個填進來只會多一個
|
||||
# 錯誤來源,那幾支本來就由接線設定明確帶 JSC_CLI。
|
||||
cli_name() {
|
||||
if [ -n "${JSC_CLI:-}" ]; then printf '%s' "$JSC_CLI"
|
||||
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then printf 'claude'
|
||||
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || [ -n "${CLAUDECODE:-}" ] \
|
||||
|| [ -n "${CLAUDE_CODE_SESSION_ID:-}" ]; then printf 'claude'
|
||||
else printf 'unknown'; fi
|
||||
}
|
||||
|
||||
|
||||
+98
-20
@@ -3,18 +3,37 @@
|
||||
#
|
||||
# 判準是階段的「能力標籤」,不是模型名稱:
|
||||
# - 標籤資料讀 $JSC_HOME/model-tags.tsv,由 jsc-cli/tools/model-tags.sh sync 產生。
|
||||
# - 目前模型 id 取自可驗證紀錄。優先讀 transcript,其次讀 hook stdin JSON 與 Codex 本機
|
||||
# session 記錄。只有這些來源都失敗時,才接受 JSC_MODEL 這個人工覆寫。
|
||||
# - 目前模型 id 取自可驗證紀錄,而且只讀屬於當前 CLI 的那一份(判定鏈見 lib.sh 的
|
||||
# current_model_report)。claude 讀 transcript 與 hook stdin,codex 讀 hook stdin 與
|
||||
# 自己的 session 記錄,其餘 CLI 本機沒有可讀的紀錄。只有這些來源都失敗時,才接受
|
||||
# JSC_MODEL 這個人工覆寫。
|
||||
# - 不採用對話內容裡模型自稱的 id。自我回報無法驗證,等同沒有閘門。
|
||||
# - 鎖存的是「階段的必要標籤」,不是「上鎖那一刻的模型」。鎖當時的模型只留作記錄,
|
||||
# 否則用不合格的模型起跑就會把自己鎖成合格,閘門永遠通過。
|
||||
#
|
||||
# 狀態檔:$JSC_HOME/sessions/{sid}.stage,單行「{stage}<TAB>{必要標籤}<TAB>{上鎖時的模型}<TAB>{模型來源}」。
|
||||
# 政策是 fail-closed:不知道能力就擋下,知道才比對能力標籤。不知道能力有三種,一律擋下——
|
||||
# 判不出是哪一支 CLI、判不出目前模型、模型判得出來但不在能力標籤表上。放行任何一種,用不
|
||||
# 合格的模型跑階段就查不出來,閘門形同虛設。代價是被擋住的人可能不知道怎麼脫困,所以每一則
|
||||
# 擋下的訊息都要帶逃生門:設 JSC_MODEL 人工指明模型,或執行本檔的 unlock 解除該階段的鎖。
|
||||
#
|
||||
# 狀態檔:$JSC_HOME/sessions/{cli}-{sid}.stage,單行
|
||||
# 「{stage}<TAB>{必要標籤}<TAB>{上鎖時的模型}<TAB>{模型來源}」。
|
||||
# 檔名帶 CLI 代號的理由見 lib.sh 的 stage_state_file:session id 判不出時會退回 default,
|
||||
# 不分 CLI 就會共用同一支 default.stage,一支上的鎖擋到另一支。舊路徑
|
||||
# $JSC_HOME/sessions/{sid}.stage 仍讀得到(往後相容),unlock 也會一併清掉,
|
||||
# 但新的寫入一律走新路徑。
|
||||
#
|
||||
# 用法:
|
||||
# sdlc-gate.sh lock {stage} 階段閘門:比對實際模型與該階段必要標籤,通過才上鎖。
|
||||
# exit 0 = 通過並已上鎖;exit 1 = 未通過,呼叫端必須停止流程。
|
||||
# sdlc-gate.sh unlock 移除狀態檔(被擋住又確定要放行時的逃生門)。
|
||||
# sdlc-gate.sh unlock [{狀態檔}]
|
||||
# 移除狀態檔(被擋住又確定要放行時的逃生門)。不帶參數時清掉這個
|
||||
# 工作階段的新舊兩份;帶參數時清掉指定的那一份。
|
||||
# 為什麼要收參數:hook 拿得到 stdin 的 session id 與 JSC_CLI,
|
||||
# 手動執行拿不到,兩邊算出來的檔名可能不同,逃生門就按不到真正
|
||||
# 擋人的那一支。所以擋人訊息會把實際讀到的路徑一起印出來,
|
||||
# 照抄就解得開。參數只收 sessions 目錄底下的 .stage 檔,
|
||||
# 避免這個逃生門變成任意刪檔的工具。
|
||||
# sdlc-gate.sh check hook 模式(UserPromptSubmit):模型不符即擋下該輪提示。
|
||||
# sdlc-gate.sh report 印出 {sid} {stage} {必要標籤} {模型 id} {模型來源} {判定};無鎖不印。
|
||||
#
|
||||
@@ -39,9 +58,10 @@
|
||||
# 結束碼(一個子命令一列):
|
||||
# lock 0=通過並已上鎖 1=未通過,呼叫端必須停止流程(階段名不合法、讀不到該
|
||||
# 階段的必要標籤、判定不出目前模型、模型不在標籤表上、缺標籤、寫不進狀態檔)
|
||||
# unlock 0=永遠成功,狀態檔不存在也算
|
||||
# check 0=放行 2=擋下該輪提示。安靜放行:沒有階段鎖、舊格式狀態檔讀不出必要標籤、
|
||||
# 必要標籤是 any、判定不出目前模型(只提醒不擋)
|
||||
# unlock 0=永遠成功,狀態檔不存在也算;不帶參數時新舊兩種路徑都會清掉
|
||||
# 2=參數不是 sessions 目錄底下的 .stage 檔
|
||||
# check 0=放行 2=擋下該輪提示。安靜放行只剩三種:沒有階段鎖、舊格式狀態檔讀不出
|
||||
# 必要標籤、必要標籤是 any。判定不出目前模型改為擋下,見上面的 fail-closed
|
||||
# report 0=永遠成功,無鎖就不印
|
||||
# wp-lock 0=已記下 2=存取庫不是 {owner}/{repo}、PR 編號不是數字、寫不進狀態檔
|
||||
# wp-unlock 0=已結清,狀態檔不存在也算 2=存取庫或 PR 編號格式錯誤
|
||||
@@ -112,7 +132,9 @@ case "${1:-}" in
|
||||
*) read_stdin ;;
|
||||
esac
|
||||
sid=$(session_id)
|
||||
state="$JSC_HOME/sessions/$sid.stage"
|
||||
# 讀寫兩條路徑分開:寫入一律走帶 CLI 代號的新路徑,讀取則允許退回舊路徑(往後相容)。
|
||||
state=$(stage_state_read_file)
|
||||
state_new=$(stage_state_file)
|
||||
TAGS_TSV="$JSC_HOME/model-tags.tsv"
|
||||
STAGES="plan analyze implement maintain"
|
||||
|
||||
@@ -182,8 +204,33 @@ model_verdict() { # $1=stage $2=req $3=model
|
||||
[ -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
|
||||
# 逃生門說明。被擋住的人多半不熟這套東西,訊息要讓他照著做就脫困,所以兩條路都寫成
|
||||
# 可以照抄的指令,而且把「可以填什麼」一併列出來。
|
||||
gate_escape_hint() { # $1=必要標籤 $2=擋人的那一支狀態檔(可省略)
|
||||
_ok=$(eligible_models "$1")
|
||||
echo "逃生門二選一:"
|
||||
echo " 1. 人工指明模型:設環境變數 JSC_MODEL={模型 id} 之後重來一次。標籤表上合格的模型 id:${_ok:-(表上沒有合格模型,請先補表)}。"
|
||||
# 路徑照抄進指令裡:手動執行時算出來的檔名跟 hook 算出來的可能不同,不點名就解不到那一支。
|
||||
if [ -n "${2:-}" ]; then
|
||||
echo " 2. 解除這個階段的鎖:執行 sh $0 unlock $2。解除之後這道閘門就不再擋。"
|
||||
else
|
||||
echo " 2. 解除這個階段的鎖:執行 sh $0 unlock。解除之後這道閘門就不再擋。"
|
||||
fi
|
||||
}
|
||||
|
||||
# 判不出 CLI 時追加一句。這是「不知道能力」的第一種,成因與修法都跟另外兩種不同:
|
||||
# 不知道是哪一支 CLI,就不知道該讀誰的模型紀錄,接線設定要把代號帶上。
|
||||
gate_cli_hint() {
|
||||
[ "$(cli_name)" = unknown ] || return 0
|
||||
echo "另外:目前判不出是哪一支 CLI(環境變數 JSC_CLI 沒有設),所以連該讀哪一份模型紀錄都不知道。接線設定應以 JSC_CLI={CLI 代號} 帶上。"
|
||||
}
|
||||
|
||||
model_fail_message() { # $1=stage $2=checked $3=必要標籤
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:判定不出目前實際使用的模型,無法驗證是否符合階段「$1」(需要 ${3:-未知})。已檢查來源:${2:-無}。本次不進行任何工作。"
|
||||
gate_cli_hint
|
||||
gate_escape_hint "${3:-any}"
|
||||
} >&2
|
||||
}
|
||||
|
||||
# --- 工作包 PR 閘門(狀態檔:$JSC_HOME/wp/{owner}-{repo}-{index}.pr,一個工作包一支) ---
|
||||
@@ -291,14 +338,17 @@ case "${1:-check}" in
|
||||
src=$(printf '%s' "$info" | cut -f2)
|
||||
checked=$(printf '%s' "$info" | cut -f3-)
|
||||
if [ -z "$cur" ]; then
|
||||
model_fail_message "$stage" "$checked"
|
||||
model_fail_message "$stage" "$checked" "$req"
|
||||
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
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:模型「$cur」(來源 ${src:-未知})不在能力標籤表上,無法判定是否夠格跑階段「$stage」(需要 $req)。請把該模型補進 jsc-cli/references/model-tags.md 後執行 model-tags.sh sync,本次不進行任何工作。"
|
||||
gate_escape_hint "$req"
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
miss=$(missing_tags "$req" "$have")
|
||||
@@ -309,13 +359,29 @@ case "${1:-check}" in
|
||||
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; }
|
||||
printf '%s\t%s\t%s\t%s\n' "$stage" "$req" "$cur" "$src" > "$state_new" 2>/dev/null || {
|
||||
echo "[jsc][SDLC 閘門][ERR]:寫不進狀態檔 $state_new,階段鎖未生效。" >&2; exit 1; }
|
||||
echo "[jsc][SDLC 閘門][OK]:階段「$stage」通過(需要 $req,目前模型 $cur,來源 $src),已上鎖。"
|
||||
exit 0 ;;
|
||||
|
||||
unlock)
|
||||
rm -f "$state" 2>/dev/null || true
|
||||
target="${2:-}"
|
||||
if [ -n "$target" ]; then
|
||||
# 只認「上一層目錄叫 sessions、副檔名是 .stage」的路徑:逃生門不該順便變成任意刪檔的
|
||||
# 工具。判準刻意不寫成「必須在這支行程算出來的 $JSC_HOME 底下」——擋人的是 hook,
|
||||
# 解鎖的是人在殼層手動執行,兩邊的 JSC_HOME 未必相同,綁上去就會把照抄訊息的人擋掉,
|
||||
# 那正是這個逃生門要避免的事。
|
||||
case "$target" in
|
||||
*/sessions/*.stage) ;;
|
||||
*)
|
||||
echo "[jsc][SDLC 閘門][ERR]:unlock 的參數只收 sessions 目錄底下的 .stage 檔,收到「$target」。不帶參數則清掉這個工作階段自己那一份。" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
rm -f "$target" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
# 新舊兩種路徑都要清。只清新的話,舊格式的鎖永遠解不開,被它擋住的人就沒有逃生門。
|
||||
rm -f "$state_new" "$(stage_state_legacy_file)" 2>/dev/null || true
|
||||
exit 0 ;;
|
||||
|
||||
check)
|
||||
@@ -330,15 +396,24 @@ case "${1:-check}" in
|
||||
cur=$(printf '%s' "$info" | cut -f1)
|
||||
src=$(printf '%s' "$info" | cut -f2)
|
||||
checked=$(printf '%s' "$info" | cut -f3-)
|
||||
# 判定不出模型時只提醒,不擋——否則使用者會被鎖在無法送出提示的狀態。
|
||||
# 判定不出模型就擋下。這是刻意改成 fail-closed:放行等於「換一支讀不到紀錄的 CLI
|
||||
# 就能繞過閘門」,閘門形同虛設。原本只提醒不擋,怕的是把人鎖在送不出提示的狀態——
|
||||
# 那個風險是真的,所以訊息一定要把兩條逃生門寫清楚,照著做就能脫困。
|
||||
if [ -z "$cur" ]; then
|
||||
echo "[jsc] SDLC 階段「${stage}」需要標籤「${req}」,但判定不出目前模型。已檢查來源:${checked:-無}。請確認 transcript、Codex session 記錄,或設定 JSC_MODEL 人工覆寫。"
|
||||
exit 0
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:SDLC 階段「${stage}」需要能力標籤「${req}」,但判定不出目前用的是哪個模型,也就無法確認夠不夠格。已檢查來源:${checked:-無}。本輪提示已擋下。"
|
||||
gate_cli_hint
|
||||
gate_escape_hint "$req" "$state"
|
||||
} >&2
|
||||
exit 2
|
||||
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
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:模型「${cur}」(來源 ${src:-未知})不在能力標籤表上,無法確認是否夠格跑階段「${stage}」(需要 ${req})。請補進 jsc-cli/references/model-tags.md 並執行 model-tags.sh sync。本輪提示已擋下。"
|
||||
gate_escape_hint "$req" "$state"
|
||||
} >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -346,7 +421,10 @@ case "${1:-check}" in
|
||||
[ -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
|
||||
{
|
||||
echo "[jsc][SDLC 閘門][ERR]:SDLC 階段「${stage}」需要標籤「${req}」,目前模型「${cur}」(來源 ${src:-未知})缺少「${miss}」。請切換到下列任一模型後重送:${ok:-(表上無合格模型,請補表)}。本輪提示已擋下。"
|
||||
echo "要結束本階段的鎖請執行:sh $0 unlock $state"
|
||||
} >&2
|
||||
exit 2 ;;
|
||||
|
||||
report)
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
#
|
||||
# --- stage 模式 ---
|
||||
#
|
||||
# 階段鎖狀態檔沿用 sdlc-gate.sh 那一份($JSC_HOME/sessions/{sid}.stage,單行
|
||||
# 階段鎖狀態檔沿用 sdlc-gate.sh 那一份($JSC_HOME/sessions/{cli}-{sid}.stage,單行
|
||||
# 「{階段}<TAB>{必要標籤}<TAB>{上鎖時的模型}」),這裡只讀不寫:判準與格式留在 sdlc-gate.sh,
|
||||
# 兩邊各存一份就會漂移。plan 與 analyze 是純邏輯階段,產出是 wiki 頁不是程式碼,所以那兩個
|
||||
# 階段鎖著時 Write、Edit、MultiEdit 一律擋下;implement 與 maintain 本來就要寫檔,放行。
|
||||
# 兩邊各存一份就會漂移。路徑一律取自 lib.sh 的 stage_state_read_file,連「新路徑優先、舊路徑
|
||||
# 往後相容」這條規則也不在這裡抄第二份。plan 與 analyze 是純邏輯階段,產出是 wiki 頁不是
|
||||
# 程式碼,所以那兩個階段鎖著時 Write、Edit、MultiEdit 一律擋下;implement 與 maintain
|
||||
# 本來就要寫檔,放行。
|
||||
#
|
||||
# --- review 模式 ---
|
||||
#
|
||||
@@ -124,16 +126,18 @@ write_tool_or_exit() {
|
||||
# ── stage:plan 與 analyze 階段鎖存在時擋下寫檔
|
||||
if [ "$mode" = stage ]; then
|
||||
write_tool_or_exit
|
||||
state="$JSC_HOME/sessions/$(session_id).stage"
|
||||
state=$(stage_state_read_file)
|
||||
[ -f "$state" ] && [ -r "$state" ] || exit 0
|
||||
stage=$(cut -f1 "$state" 2>/dev/null | head -n1)
|
||||
case "$stage" in
|
||||
plan|analyze) ;;
|
||||
*) exit 0 ;; # implement 與 maintain 本來就要寫檔;讀不出階段也放行
|
||||
esac
|
||||
# 解除指令帶上實際讀到的狀態檔路徑:擋人的是 hook,解鎖的是人在殼層手動執行,兩邊算出來
|
||||
# 的檔名未必相同,不點名就會解到別的地方去。
|
||||
deny "目前鎖在 SDLC「$stage」階段,這個階段只產出計畫或分析頁,不寫檔案。本次寫入已擋下。" \
|
||||
"改法:把結論寫進該階段的 wiki 頁;真的要動程式碼請先進入 implement 階段。
|
||||
解除階段鎖:jsc-hooks/hooks/sdlc-gate.sh unlock"
|
||||
解除階段鎖:sh $HERE/sdlc-gate.sh unlock $state"
|
||||
fi
|
||||
|
||||
# ── review:稽核類技能執行中擋下寫檔
|
||||
|
||||
Reference in New Issue
Block a user