feat(write-guard): 新增寫入與提交閘門,把三條只寫在內文的規則落到程式層

新增第九支 hook,共四種模式。stage 在計畫與分析階段鎖著時擋下寫檔。
review 在稽核類技能執行中擋下寫檔。commit 擋下「一次加入全部變更再提交」的
單一指令,也擋下含簡體字、亂碼或非 UTF-8 編碼的提交訊息。
release 不接 hook,由稽核技能收尾時自己呼叫,清掉認人用的那份紀錄,
讓呼叫端接手修改時不會被剛跑完的稽核擋住。

這三條規則原本只寫在技能內文,靠模型自律。稽核技能會順手改程式碼,
計畫階段會寫出不該寫的檔案,整包提交會把型別與功能分組壓成一次。
規則要真的生效,就得由程式擋。release 模式是必要的:紀錄記的是最近一次
載入的技能,不是還在跑的技能,沒有它,稽核跑完之後呼叫端每一次寫入都被擋,
閘門會把解除自己的路徑一起鎖掉。

接線設定把 stage 與 review 接在寫檔工具,把 commit 接在指令工具。
閘門只讀階段閘門的狀態鎖與技能用量的紀錄,不自己寫狀態檔;
簡繁與編碼判定整段轉呼叫語言守門,不留第二份字表。
接線腳本的接線、唯讀盤點與冒煙各補上這一支,冒煙自備暫時的狀態目錄,
逐條比對每種模式的結束碼。接線腳本另補唯讀模式,體檢類技能全程帶著它跑,
清除與接線一律拒絕並回非零,子命令打錯一個字也改不到環境。
冒煙改成自己數結果行並自我斷言,散文只引用那一行,不再各抄一份數字。

限制據實寫在檔頭:只有 claude 有寫檔前置鉤子,其餘四支 CLI 一條都接不上,
那四支上這三條規則仍只剩技能內文。
This commit is contained in:
2026-08-31 11:15:24 +08:00
parent 6b272974b5
commit b5219f6549
3 changed files with 489 additions and 24 deletions
+22
View File
@@ -73,6 +73,28 @@
"command": "sh -c 'root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/sdlc-gate.sh\" wp-check skill'" "command": "sh -c 'root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/sdlc-gate.sh\" wp-check skill'"
} }
] ]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "sh -c 'root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/write-guard.sh\" stage'"
},
{
"type": "command",
"command": "sh -c 'root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/write-guard.sh\" review'"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "sh -c 'root=\"${CLAUDE_PLUGIN_ROOT:-${JSC_HOME:-$HOME/.jsc}/current/jsc-hooks}\"; exec sh \"$root/hooks/write-guard.sh\" commit'"
}
]
} }
], ],
"PostToolUse": [ "PostToolUse": [
+280
View File
@@ -0,0 +1,280 @@
#!/usr/bin/env sh
# write-guard.sh — 寫入與提交的前置閘門(PreToolUse)。三種擋人模式接在兩個 matcher 上,
# 另有一個給技能收尾呼叫的解除模式。
#
# 用法:
# write-guard.sh stage matcher Write|Edit|MultiEdit:plan 與 analyze 階段鎖存在時擋下寫檔
# write-guard.sh review matcher Write|Edit|MultiEdit:稽核類技能執行中擋下寫檔
# write-guard.sh commit matcher Bash:擋下 git add -A 後的單次提交,以及含簡體字或亂碼的提交訊息
# write-guard.sh release 不接 hook,由稽核技能收尾時自己呼叫:清掉 review 模式認人用的那份
# 紀錄,一律 exit 0,紀錄本來就不存在也算成功
#
# 輸入相容(比照其他 hook,stdin JSON 與環境變數都收,缺欄位一律安靜降級 exit 0):
# 工具名 JSC_TOOL_NAME、TOOL_NAME、stdin 的 tool_name
# 技能名 JSC_SKILL、SKILL、stdin 的 skill
# 指令 JSC_TOOL_COMMAND、stdin 的 command
#
# 結束碼:0=放行、解除完成、資料不足或不認得的模式;2=擋下,訊息走 stderr。
# 逃生門:JSC_WRITE_GUARD=off,三種擋人模式全部略過(release 不受影響,清紀錄擋不到任何人)。
# review 模式另有 JSC_WRITE_GUARD_TTL(預設 900 秒),見下方「稽核技能的時效」。
#
# 覆蓋範圍要據實看待:只有 claude 有 PreToolUse,這道閘門只在 claude 上擋得下來。
# codex、copilot、antigravity、kiro 都沒有 pre-tool 事件,三種模式在那四支上一次都擋不到,
# 規則只剩 SKILL.md 的散文,回報時不得暗示每支 CLI 都擋得住。
#
# --- stage 模式 ---
#
# 階段鎖狀態檔沿用 sdlc-gate.sh 那一份($JSC_HOME/sessions/{sid}.stage,單行
# 「{階段}<TAB>{必要標籤}<TAB>{上鎖時的模型}」),這裡只讀不寫:判準與格式留在 sdlc-gate.sh,
# 兩邊各存一份就會漂移。plan 與 analyze 是純邏輯階段,產出是 wiki 頁不是程式碼,所以那兩個
# 階段鎖著時 Write、Edit、MultiEdit 一律擋下;implement 與 maintain 本來就要寫檔,放行。
#
# --- review 模式 ---
#
# 目前技能取自 skill-usage.sh 已經記下的那一份($JSC_HOME/sessions/{sid}.lastskill),
# 環境變數餵得到技能名時優先用環境變數。code-review 與 api-doc 只回報發現、不改程式碼,
# 執行中出現寫入就是越權,擋下。
#
# comment-cleanup 不擋:它本來就要改檔,只是限定「僅註解行」。要精確判定得先解析工具參數裡
# 帶跳脫字元的整份新內容,再逐語言判斷哪幾行是註解——判錯就會擋掉合法的清理,代價比漏擋大。
# 這一支因此只放行,寫入範圍由 SKILL.md 的散文與後續審查把關,不在這裡硬做。
#
# 稽核技能的時效:沒有「技能結束」事件可讀,只有「最近一次呼叫的技能」這個事實。不設界線的話,
# 稽核技能跑完之後每一次寫檔都會被擋到下一支技能被呼叫為止。所以紀錄超過 JSC_WRITE_GUARD_TTL
# 秒就當那支技能早已跑完,放行;取不到紀錄時間也放行。
#
# --- release 模式 ---
#
# 介面:write-guard.sh release,不吃其他參數、不接任何 hook 事件,由呼叫端自己執行。
# 呼叫端是 jsc-review:code-review 與 jsc-review:api-doc,兩支在收尾(把發現清單交回呼叫端)
# 那一步各呼叫一次。做的事只有一件:刪掉 $JSC_HOME/sessions/{sid}.lastskill。
#
# 為什麼要有這個模式:review 模式靠那份紀錄認人,而 skill-usage.sh 記的是「最近一次載入的
# 技能」,不是「還在跑的技能」。code-review 的契約是只回報、修不修由呼叫端決定,稽核結束後
# 呼叫端本來就要動手改——那一刻紀錄仍寫著 code-review,JSC_WRITE_GUARD_TTL 內每一次寫入
# 都被擋,解除路徑只剩逃生門或空等。閘門不得把解除自己的路徑一起鎖掉,所以補一個由呼叫端
# 自己按的解除鍵。
# 只刪那一份紀錄,不碰階段鎖:階段鎖歸 sdlc-gate.sh unlock 管,兩件事混在一起會互相解除。
#
# --- commit 模式 ---
#
# 擋兩件事:
# 1. 同一道指令裡同時有「git add -A(或 --all、.)」與「git commit」。那等於把所有待提交
# 變更併成一次提交,型別與功能分組就消失了。跨兩次工具呼叫的同一組動作不擋——那要記
# 跨呼叫狀態,而被擋下的人沒有辦法讓那個狀態自己消失,閘門會把解除自己的路徑一起鎖掉。
# 2. 提交訊息含簡體字、亂碼或非 UTF-8 編碼。判定整段轉呼叫 lang-guard.sh(字表在
# hooks/simplified.txt),這裡不抄第二份樣式;連帶地 JSC_LANG_GUARD=off 也會關掉這一項,
# 因為那本來就是同一條規則。
set -u
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
# lib.sh 讀不到就直接放行。這裡不能寫成「. lib.sh || true」:dash 的 `.` 找不到檔案時會結束
# 整支 shell,後面的 || true 一次都跑不到,2>/dev/null 還把原因蓋掉,三種模式全部變成無訊息
# 的 exit 2——而 PreToolUse 的 exit 2 正是「擋下」,等於每一次寫檔與提交都被無聲擋死。
[ -r "$HERE/lib.sh" ] || exit 0
. "$HERE/lib.sh"
# lib.sh 沒載到時這個變數就沒人設,下面兩個模式都要用它組狀態檔路徑,補一份同樣的預設值。
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
mode="${1:-}"
case "$mode" in
stage|review|commit|release) ;;
*) exit 0 ;; # 不認得的模式一律安靜放行,不中斷宿主 CLI
esac
read_stdin 2>/dev/null || STDIN_JSON=""
# ── release:稽核技能收尾時清掉「目前技能」紀錄,解除 review 模式的擋下
#
# 排在逃生門之前,也不受 JSC_WRITE_GUARD=off 影響:清一筆紀錄從來不會擋到任何人,
# 而收尾呼叫失敗才是真的麻煩——閘門關著的機器上跑過一輪,紀錄留著,下次開啟就自鎖。
if [ "$mode" = release ]; then
rm -f "$JSC_HOME/sessions/$(session_id).lastskill" 2>/dev/null || true
exit 0
fi
[ "${JSC_WRITE_GUARD:-on}" = "off" ] && exit 0
deny() { # $1=擋下的理由 $2=修法
printf '[jsc][寫入閘門][ERR]:%s\n' "$1" >&2
printf '%s\n' "$2" >&2
printf '確定要略過這道閘門:JSC_WRITE_GUARD=off\n' >&2
exit 2
}
# 檔案的最後修改時間(epoch 秒)。GNU 與 BSD 的取法不同,三種都試過才放棄;
# 取不到就不輸出,呼叫端當成無法判定並放行。
file_mtime() { # $1=檔案
_m=$(date -r "$1" +%s 2>/dev/null)
[ -n "$_m" ] || _m=$(stat -c %Y "$1" 2>/dev/null)
[ -n "$_m" ] || _m=$(stat -f %m "$1" 2>/dev/null)
printf '%s' "$_m"
}
# 只在寫檔類工具上判定。工具名取不到就當成沒有篩選條件,交給後面的狀態判定——
# 接線的 matcher 已經先篩過一輪,這裡再擋一次只會把用環境變數餵資料的 CLI 全部放掉。
write_tool_or_exit() {
_t="${JSC_TOOL_NAME:-${TOOL_NAME:-$(json_str tool_name)}}"
case "$_t" in
""|Write|Edit|MultiEdit) return 0 ;;
*) exit 0 ;;
esac
}
# ── stage:plan 與 analyze 階段鎖存在時擋下寫檔
if [ "$mode" = stage ]; then
write_tool_or_exit
state="$JSC_HOME/sessions/$(session_id).stage"
[ -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
deny "目前鎖在 SDLC「$stage」階段,這個階段只產出計畫或分析頁,不寫檔案。本次寫入已擋下。" \
"改法:把結論寫進該階段的 wiki 頁;真的要動程式碼請先進入 implement 階段。
解除階段鎖:jsc-hooks/hooks/sdlc-gate.sh unlock"
fi
# ── review:稽核類技能執行中擋下寫檔
if [ "$mode" = review ]; then
write_tool_or_exit
skill="${JSC_SKILL:-${SKILL:-$(json_str skill)}}"
if [ -z "$skill" ]; then
last="$JSC_HOME/sessions/$(session_id).lastskill"
if [ -f "$last" ] && [ -r "$last" ]; then
mt=$(file_mtime "$last")
now=$(now_epoch)
if [ -n "$mt" ] && [ -n "$now" ]; then
age=$((now - mt))
[ "$age" -lt "${JSC_WRITE_GUARD_TTL:-900}" ] && skill=$(cat "$last" 2>/dev/null)
fi
fi
fi
[ -n "$skill" ] || exit 0
case "$skill" in
jsc-review:code-review|code-review)
deny "jsc-review:code-review 執行中。這支技能只回報發現,修不修由呼叫端決定,執行中不寫檔。本次寫入已擋下。" \
"改法:先讓稽核跑完並收下 file:line、嚴重度與重構手法,再由呼叫端決定要不要改。" ;;
jsc-review:api-doc|api-doc)
deny "jsc-review:api-doc 執行中。這支技能只稽核 Swagger 文件屬性,從不修改程式碼。本次寫入已擋下。" \
"改法:先讓稽核跑完並收下缺漏清單,再由呼叫端決定要不要補。" ;;
esac
exit 0
fi
# ── commit:擋下 git add -A 後的單次提交,與含簡體字或亂碼的提交訊息
# 從 stdin JSON 取帶跳脫字元的字串欄位。lib.sh 的 json_str 以 [^"]* 比對,遇到訊息裡的 \"
# 就在那裡截斷,提交訊息會少掉後半段——而訊息內容正是這裡要檢查的東西,所以自己解一次跳脫。
json_escaped_str() { # $1=欄位名
printf '%s' "$STDIN_JSON" | awk -v key="$1" '
{ s = s $0 "\n" }
END {
n = length(s); i = 1; found = 0
while (i <= n) {
if (substr(s, i, 1) != "\"") { i++; continue }
buf = ""; i++
while (i <= n) {
c = substr(s, i, 1)
if (c == "\\") {
e = substr(s, i + 1, 1)
if (e == "n") buf = buf "\n"
else if (e == "t") buf = buf "\t"
else if (e == "r") buf = buf "\r"
else if (e == "u") { i += 6; continue }
else buf = buf e
i += 2; continue
}
if (c == "\"") { i++; break }
buf = buf c; i++
}
if (found) { printf "%s", buf; exit }
j = i
while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++
if (buf == key && substr(s, j, 1) == ":") {
k = j + 1
while (k <= n && substr(s, k, 1) ~ /[ \t\r\n]/) k++
if (substr(s, k, 1) != "\"") { i = k; continue }
found = 1; i = k
}
}
}'
}
cmd="${JSC_TOOL_COMMAND:-$(json_escaped_str command)}"
[ -n "$cmd" ] || exit 0
case "$cmd" in
*git*) ;;
*) exit 0 ;; # 不是 git 指令就不關這道閘門的事
esac
# 把所有待提交變更一次加進索引的三種寫法。-A 也認 -vA 這類併寫的短旗標。
has_add_all() { # $1=指令
printf '%s' "$1" \
| grep -qE 'git[[:space:]]+add[[:space:]]+(-[A-Za-z]*A([[:space:]]|$)|--all([[:space:]]|$)|\.([[:space:]]|$))'
}
has_commit() { # $1=指令
printf '%s' "$1" | grep -qE 'git[[:space:]]+commit([[:space:]]|$)'
}
if has_add_all "$cmd" && has_commit "$cmd"; then
deny "這道指令把全部變更一次加進索引再提交,型別與功能分組會全部消失。本次執行已擋下。" \
"改法:依 conventional type 與功能分組,逐組 git add {檔案} 再各自 git commit。
分組與訊息格式交給 /jsc-git:commit 處理。"
fi
# 提交訊息:取 -m 後面那一段。帶引號就讀到成對的引號為止,沒帶引號就讀到下一個空白。
_sq=$(printf '\047')
commit_message() { # $1=指令
printf '%s' "$1" | awk -v sq="$_sq" '
{
s = $0; n = length(s)
i = index(s, "-m")
if (i == 0) exit
i += 2
while (i <= n && substr(s, i, 1) ~ /[ \t=]/) i++
q = substr(s, i, 1)
if (q == "\"" || q == sq) {
i++
while (i <= n) {
c = substr(s, i, 1)
if (c == "\\") { out = out substr(s, i + 1, 1); i += 2; continue }
if (c == q) break
out = out c; i++
}
} else {
while (i <= n && substr(s, i, 1) !~ /[ \t]/) { out = out substr(s, i, 1); i++ }
}
printf "%s", out
}'
}
has_commit "$cmd" || exit 0
msg=$(commit_message "$cmd")
[ -n "$msg" ] || exit 0
# 簡體字、亂碼與編碼判定整段轉呼叫 lang-guard.sh,樣式與字表都不在這裡留第二份。
# 那支腳本吃的是檔案,所以訊息先落成暫存檔;建不出暫存檔就放行,回報不該再變成一次失敗。
lang_hits() { # $1=文字;有問題就把證據印到 stdout 並回傳 1
[ -f "$HERE/lang-guard.sh" ] || return 0
_t=$(mktemp 2>/dev/null) || return 0
printf '%s\n' "$1" > "$_t" 2>/dev/null || { rm -f "$_t"; return 0; }
_o=$(JSC_CHANGED_FILE="$_t" sh "$HERE/lang-guard.sh" </dev/null 2>&1)
_rc=$?
rm -f "$_t"
# 只有 exit 2 是「確定命中」。exit 0 是乾淨或資料不足,其餘結束碼代表那支腳本自己出狀況,
# 兩種都放行:拿判不出來的結果擋提交,等於把護欄變成故障點。
[ "$_rc" -eq 2 ] || return 0
# 只留命中證據:開頭那句與 lang-guard.sh 自己的修法兩行由本檔的訊息取代,重複印只是噪音。
printf '%s\n' "$_o" | grep -vF "$_t" | grep -v '^\[jsc\]' \
| grep -v '^ 修法:' | grep -v '^ 規則正文' | head -n 4
return 1
}
evidence=$(lang_hits "$msg") && exit 0
deny "提交訊息有簡體字、亂碼或編碼問題。本次執行已擋下。
$evidence" \
"改法:訊息改寫成繁體中文、UTF-8、無亂碼。字表在 jsc-hooks 的 hooks/simplified.txt,
規則正文見 jsc-meta 的 references/ste100.md。"
+187 -24
View File
@@ -3,9 +3,12 @@
# 用法: # 用法:
# wire-cli.sh {claude|codex|copilot|antigravity|kiro} 接線 # wire-cli.sh {claude|codex|copilot|antigravity|kiro} 接線
# wire-cli.sh purge {claude|codex|copilot|antigravity|kiro} 備份後移除該 CLI 的所有 hook # wire-cli.sh purge {claude|codex|copilot|antigravity|kiro} 備份後移除該 CLI 的所有 hook
# wire-cli.sh smoke {claude|codex|copilot|antigravity|kiro} 跑一輪八支 hook,驗執行期 # wire-cli.sh smoke {claude|codex|copilot|antigravity|kiro} 跑一輪九支 hook,驗執行期
# wire-cli.sh status {claude|codex|copilot|antigravity|kiro} 唯讀盤點接線現況,不寫檔也不執行 hook # wire-cli.sh status {claude|codex|copilot|antigravity|kiro} 唯讀盤點接線現況,不寫檔也不執行 hook
# #
# JSC_READONLY=1 時只准 status 與 smoke,purge 與接線一律拒絕並回 exit 6。體檢類技能全程帶著
# 這個變數跑,「子命令打錯一個字就重新接線或刪檔」的風險就由程式擋掉,不靠呼叫端自我約束。
#
# purge 移除的是「所有 hook」,含非 jsc 的第三方項目。安裝一律先 purge 再接線:混著別人的 # purge 移除的是「所有 hook」,含非 jsc 的第三方項目。安裝一律先 purge 再接線:混著別人的
# hook 接線,出錯時分不清是誰的 hook 壞掉,也修不了。移除前每個要動的檔案先原樣複製到 # hook 接線,出錯時分不清是誰的 hook 壞掉,也修不了。移除前每個要動的檔案先原樣複製到
# $JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/,備份失敗就不移除。 # $JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/,備份失敗就不移除。
@@ -17,9 +20,15 @@
# restart-gate.sh 的部署後重啟閘門同法:當前 CLI 沒有狀態檔、只有別支 CLI 有狀態檔、當前 # restart-gate.sh 的部署後重啟閘門同法:當前 CLI 沒有狀態檔、只有別支 CLI 有狀態檔、當前
# CLI 那份存在、豁免技能、逃生門、取不到技能名各跑一次,再驗狀態檔本身在不在——清除只刪自己 # CLI 那份存在、豁免技能、逃生門、取不到技能名各跑一次,再驗狀態檔本身在不在——清除只刪自己
# 那一份、別支那一份留著、舊格式的單一狀態檔照樣擋得下來且清得掉。 # 那一份、別支那一份留著、舊格式的單一狀態檔照樣擋得下來且清得掉。
# write-guard.sh 的四種模式同法:階段鎖、稽核技能、提交指令與 release 解除各自的判定路徑,
# 都用暫時的 $JSC_HOME 跑過一次並比對結束碼。
#
# smoke 自己數結果行並自我斷言:`lines<TAB>{數量}` 那一行印的是其後 `[jsc]` 結果行的實際條數,
# 與腳本內宣告的預期條數逐類比對,不符就回非零。數字寫在腳本裡、由腳本自己印,散文引用那一行
# 就好,不必在 SKILL.md 或 README 各抄一份——抄了就會在加減判定路徑時漂移。
# #
# 接線行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加): # 接線行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加):
# claude — 什麼都不用寫,hooks.json 已自動接線八支 hook # claude — 什麼都不用寫,hooks.json 已自動接線九支 hook
# codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時, # codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時,
# 結束時收尾掃一次註解範圍與繁中編碼);在 config.toml 設 notify(每輪補 # 結束時收尾掃一次註解範圍與繁中編碼);在 config.toml 設 notify(每輪補
# session-timer.sh start 再 mark,最後 comment-scope.sh sweep 與 # session-timer.sh start 再 mark,最後 comment-scope.sh sweep 與
@@ -36,10 +45,11 @@
# 皆帶 JSC_CLI=kiro # 皆帶 JSC_CLI=kiro
# #
# 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護: # 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護:
# claude 八支 hook 全接,回報 wired # claude 九支 hook 全接,回報 wired
# codex、copilot、antigravity、kiro 只有別名、notify 或規則檔,接不上 PreToolUse、PostToolUse # codex、copilot、antigravity、kiro 只有別名、notify 或規則檔,接不上 PreToolUse、PostToolUse
# 與 UserPromptSubmit,版本前置檢查、部署後重啟閘門與 # 與 UserPromptSubmit,版本前置檢查、部署後重啟閘門、寫入與
# SDLC 模型鎖都沒接上,一律回報 degraded 並在 reason 講明。 # 提交閘門與 SDLC 模型鎖都沒接上,一律回報 degraded 並在
# reason 講明。
# 重啟閘門在這四個 CLI 上一次技能呼叫都擋不下來:狀態檔照樣 # 重啟閘門在這四個 CLI 上一次技能呼叫都擋不下來:狀態檔照樣
# 寫、下次工作階段開始照樣清,只是中間沒有任何判定點,重啟 # 寫、下次工作階段開始照樣清,只是中間沒有任何判定點,重啟
# 只能靠 /jsc-cli:deploy 收尾的提示自己動手 # 只能靠 /jsc-cli:deploy 收尾的提示自己動手
@@ -65,8 +75,9 @@
# 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過) # 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過)
# 4=failed(寫入或驗證沒過,接線沒生效;由 hooks-install 呼叫 report-error.sh 回報) # 4=failed(寫入或驗證沒過,接線沒生效;由 hooks-install 呼叫 report-error.sh 回報)
# 結束碼(purge): 0=purged 2=用法錯誤 3=skipped 4=failed # 結束碼(purge): 0=purged 2=用法錯誤 3=skipped 4=failed
# 結束碼(smoke): 0=ok 2=用法錯誤 4=failed # 結束碼(smoke): 0=ok 2=用法錯誤 4=failed(含結果行數與預期不符)
# 結束碼(status): 0=wired 1=degraded 2=用法錯誤 3=skipped 5=unwired(該接的段落缺了至少一項) # 結束碼(status): 0=wired 1=degraded 2=用法錯誤 3=skipped 5=unwired(該接的段落缺了至少一項)
# 結束碼(唯讀模式): 6=readonly(JSC_READONLY=1 之下拒絕 purge 與接線),status 與 smoke 不受影響
# status 之外的動作都會寫檔,體檢類技能(/jsc-cli:doctor)只能呼叫 status。判讀邏輯跟接線 # status 之外的動作都會寫檔,體檢類技能(/jsc-cli:doctor)只能呼叫 status。判讀邏輯跟接線
# 共用同一組檔案位置與標記字串,分兩份實作就會各自漂移,體檢說沒接、實際上接著。 # 共用同一組檔案位置與標記字串,分兩份實作就會各自漂移,體檢說沒接、實際上接著。
# #
@@ -102,6 +113,20 @@ case "$cli" in
*) usage ;; *) usage ;;
esac esac
# 唯讀契約在程式層把關,不靠呼叫端記得只打 status。子命令解析完就判:預設動作是接線,
# 所以少打一個子命令就會直接改環境,這個判定要擋的正是那一次手滑。
if [ "${JSC_READONLY:-}" = "1" ]; then
case "$action" in
status|smoke) ;;
*)
printf 'status=readonly reason=%s\n' "JSC_READONLY=1 之下只准 status 與 smoke,$action 會動到接線,已拒絕"
echo "[jsc] 目前是唯讀模式(JSC_READONLY=1),purge 與接線一律拒絕,環境沒有被動過。" >&2
echo "[jsc] 要盤點接線現況請用 wire-cli.sh status $cli;要驗執行期請用 wire-cli.sh smoke $cli。" >&2
echo "[jsc] 確定要改接線,請在沒有 JSC_READONLY 的環境重跑,或改走 /jsc-hooks:hooks-install。" >&2
exit 6 ;;
esac
fi
# STE100 規則段落的唯一來源:ste100-guard.sh 的實際輸出 # STE100 規則段落的唯一來源:ste100-guard.sh 的實際輸出
ste100_text() { sh "$HOOKS/ste100-guard.sh" 2>/dev/null | sed '/^exit /d'; } ste100_text() { sh "$HOOKS/ste100-guard.sh" 2>/dev/null | sed '/^exit /d'; }
@@ -716,6 +741,20 @@ if [ "$action" = smoke ]; then
smoke_out=$(mktemp) || { printf 'status=failed reason=%s\n' "無法建立暫存檔"; exit 4; } smoke_out=$(mktemp) || { printf 'status=failed reason=%s\n' "無法建立暫存檔"; exit 4; }
smoke_fails=0 smoke_fails=0
# 每一類實際跑過的結果行數。收尾時與下面宣告的預期條數逐類比對,加減判定路徑卻忘了改預期
# 就會當場失敗,散文與程式之間不會再各記一份數字。
smoke_n_hook=0; smoke_n_wp=0; smoke_n_rs=0; smoke_n_wg=0
# 預期條數(改動判定路徑時一起改):
# hook 模式 九支 hook 的每個接線模式各一條。sdlc-gate.sh、comment-scope.sh、
# lang-guard.sh 與 write-guard.sh 各有多個模式,所以比 hook 支數多
# 工作包 sdlc-gate.sh wp-check skill 的歸屬判定路徑
# 重啟閘門 restart-gate.sh 的判定、清除路徑與狀態檔範圍檢查
# 寫入閘門 write-guard.sh 三種擋人模式與 release 解除模式的判定路徑
SMOKE_EXPECT_HOOK=17
SMOKE_EXPECT_WP=6
SMOKE_EXPECT_RS=16
SMOKE_EXPECT_WG=21
# 跑一支 hook 並判定結果。$1=腳本檔名 $2=子命令(可省略) # 跑一支 hook 並判定結果。$1=腳本檔名 $2=子命令(可省略)
# $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。 # $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。
smoke_one() { smoke_one() {
@@ -726,6 +765,7 @@ if [ "$action" = smoke ]; then
# 去掃那個檔,掃到違規就 exit 2,冒煙測試變成看環境臉色,測不出腳本本身跑不跑得完。 # 去掃那個檔,掃到違規就 exit 2,冒煙測試變成看環境臉色,測不出腳本本身跑不跑得完。
_out=$(printf '{}' | JSC_CLI="$cli" JSC_SKILL="" SKILL="" JSC_CHANGED_FILE="" \ _out=$(printf '{}' | JSC_CLI="$cli" JSC_SKILL="" SKILL="" JSC_CHANGED_FILE="" \
sh "$HOOKS/$_h" $_s 2>&1); _rc=$? sh "$HOOKS/$_h" $_s 2>&1); _rc=$?
smoke_n_hook=$((smoke_n_hook + 1))
if [ "$_rc" -eq 0 ]; then if [ "$_rc" -eq 0 ]; then
printf '[jsc] %s%s:exit 0,正常。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out" printf '[jsc] %s%s:exit 0,正常。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out"
elif [ "$_h" = sdlc-gate.sh ] && [ "$_s" = check ] && [ "$_rc" -eq 2 ]; then elif [ "$_h" = sdlc-gate.sh ] && [ "$_s" = check ] && [ "$_rc" -eq 2 ]; then
@@ -742,6 +782,11 @@ if [ "$action" = smoke ]; then
# 沿用同一條例外:lang-guard.sh 掃描模式的 exit 2 是「掃到簡體字、亂碼或編碼問題」的 # 沿用同一條例外:lang-guard.sh 掃描模式的 exit 2 是「掃到簡體字、亂碼或編碼問題」的
# 設計行為。sweep 一樣看工作區乾不乾淨,髒工作區本來就會回 2,不是 hook 壞掉。 # 設計行為。sweep 一樣看工作區乾不乾淨,髒工作區本來就會回 2,不是 hook 壞掉。
printf '[jsc] %s%s:exit 2,掃到簡體字或亂碼並發出警告,屬設計行為,不算錯誤。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out" printf '[jsc] %s%s:exit 2,掃到簡體字或亂碼並發出警告,屬設計行為,不算錯誤。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out"
elif [ "$_h" = write-guard.sh ] && [ "$_rc" -eq 2 ]; then
# 同一條例外:write-guard.sh 的 exit 2 是「擋下這次寫入或提交」的設計行為。這一輪用的是
# 真正的 $JSC_HOME,機器上剛好鎖在 plan 階段、或最近一次呼叫的是稽核技能時本來就會回 2,
# 不是 hook 壞掉。三種模式的判定結果由下面自備狀態檔的那一段逐條驗。
printf '[jsc] %s%s:exit 2,擋下這次寫入或提交,屬設計行為,不算錯誤。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out"
else else
smoke_fails=$((smoke_fails + 1)) smoke_fails=$((smoke_fails + 1))
printf '[jsc] %s%s:exit %s,執行期出錯:%s\n' "$_h" "${_s:+ $_s}" "$_rc" \ printf '[jsc] %s%s:exit %s,執行期出錯:%s\n' "$_h" "${_s:+ $_s}" "$_rc" \
@@ -808,6 +853,7 @@ if [ "$action" = smoke ]; then
smoke_wp_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=JSC_WP_GATE 值(可省略) smoke_wp_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=JSC_WP_GATE 值(可省略)
_out=$(JSC_HOME="$wp_home" JSC_CLI="$cli" JSC_SKILL="$2" SKILL="$2" \ _out=$(JSC_HOME="$wp_home" JSC_CLI="$cli" JSC_SKILL="$2" SKILL="$2" \
JSC_WP_GATE="${4:-}" sh "$HOOKS/sdlc-gate.sh" wp-check skill </dev/null 2>&1); _rc=$? JSC_WP_GATE="${4:-}" sh "$HOOKS/sdlc-gate.sh" wp-check skill </dev/null 2>&1); _rc=$?
smoke_n_wp=$((smoke_n_wp + 1))
if [ "$_rc" -eq "$3" ]; then if [ "$_rc" -eq "$3" ]; then
printf '[jsc] sdlc-gate.sh wp-check skill(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out" printf '[jsc] sdlc-gate.sh wp-check skill(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out"
else else
@@ -822,7 +868,9 @@ if [ "$action" = smoke ]; then
printf 'repo=jsc/smoke\nindex=12\nwp=WP-03\n' > "$wp_home/wp/jsc-smoke-12.pr" printf 'repo=jsc/smoke\nindex=12\nwp=WP-03\n' > "$wp_home/wp/jsc-smoke-12.pr"
smoke_wp_case "PR 屬於領取中的工作包,技能 implement" implement 0 smoke_wp_case "PR 屬於領取中的工作包,技能 implement" implement 0
printf 'repo=jsc/smoke\nindex=9\nwp=WP-01\n' > "$wp_home/wp/jsc-smoke-9.pr" 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 屬於別的工作包,技能 analyze" analyze 2
# plan 只提醒不擋,理由見 sdlc-gate.sh 檔頭「plan 已從擋人名單移出」。
smoke_wp_case "PR 屬於別的工作包,技能 plan" plan 0
smoke_wp_case "PR 屬於別的工作包,技能 implement" implement 0 smoke_wp_case "PR 屬於別的工作包,技能 implement" implement 0
smoke_wp_case "逃生門 JSC_WP_GATE=off" plan 0 off smoke_wp_case "逃生門 JSC_WP_GATE=off" plan 0 off
rm -rf "$wp_home" rm -rf "$wp_home"
@@ -840,6 +888,7 @@ if [ "$action" = smoke ]; then
smoke_rs_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=JSC_RESTART_GATE 值(可省略) smoke_rs_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=JSC_RESTART_GATE 值(可省略)
_out=$(JSC_HOME="$rs_home" JSC_CLI="$cli" JSC_SKILL="$2" SKILL="$2" \ _out=$(JSC_HOME="$rs_home" JSC_CLI="$cli" JSC_SKILL="$2" SKILL="$2" \
JSC_RESTART_GATE="${4:-}" sh "$HOOKS/restart-gate.sh" </dev/null 2>&1); _rc=$? JSC_RESTART_GATE="${4:-}" sh "$HOOKS/restart-gate.sh" </dev/null 2>&1); _rc=$?
smoke_n_rs=$((smoke_n_rs + 1))
if [ "$_rc" -eq "$3" ]; then if [ "$_rc" -eq "$3" ]; then
printf '[jsc] restart-gate.sh(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out" printf '[jsc] restart-gate.sh(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out"
else else
@@ -850,6 +899,7 @@ if [ "$action" = smoke ]; then
} }
# 狀態檔在不在也要比:一支 CLI 一份的重點就在「該留的留、該刪的刪」,只看結束碼看不出來。 # 狀態檔在不在也要比:一支 CLI 一份的重點就在「該留的留、該刪的刪」,只看結束碼看不出來。
smoke_rs_file() { # $1=情境 $2=狀態檔 $3=exist 或 absent smoke_rs_file() { # $1=情境 $2=狀態檔 $3=exist 或 absent
smoke_n_rs=$((smoke_n_rs + 1))
if { [ "$3" = exist ] && [ -f "$2" ]; } || { [ "$3" = absent ] && [ ! -f "$2" ]; }; then if { [ "$3" = exist ] && [ -f "$2" ]; } || { [ "$3" = absent ] && [ ! -f "$2" ]; }; then
printf '[jsc] restart-gate.sh(%s):狀態檔 %s,與預期相同。\n' "$1" "$3" >> "$smoke_out" printf '[jsc] restart-gate.sh(%s):狀態檔 %s,與預期相同。\n' "$1" "$3" >> "$smoke_out"
else else
@@ -908,12 +958,111 @@ if [ "$action" = smoke ]; then
smoke_one lang-guard.sh prompt smoke_one lang-guard.sh prompt
smoke_one lang-guard.sh smoke_one lang-guard.sh
smoke_one lang-guard.sh sweep smoke_one lang-guard.sh sweep
# write-guard.sh 三種模式接在兩個 matcher 上,三個都要驗。這一輪用真正的 $JSC_HOME,只確認
# 腳本跑得完;擋下時的 exit 2 由上面的白名單放行,判定結果本身在下一段用暫時狀態檔逐條驗。
smoke_one write-guard.sh stage
smoke_one write-guard.sh review
smoke_one write-guard.sh commit
# 寫入與提交閘門(write-guard.sh):上面三支只證明跑得完,三種模式的判定路徑一條都沒走到。
# 這裡自備一份暫時的 $JSC_HOME 與暫時的指令字串,把每條路徑各跑一次並比對結束碼。用暫時目錄
# 是為了不動到使用者真正的階段鎖與技能紀錄——冒煙測試不該把別人的階段鎖讀成擋人的理由。
# 一律 </dev/null:三種模式都讀標準輸入,管線沒人關閉時整支卡死。
smoke_wg_case() { # $1=情境 $2=模式 $3=預期結束碼 $4=技能名 $5=指令 $6=額外環境設定(KEY=值)
# 第六個參數省略時仍要餵一個合法的 KEY=值 給 env,否則它會把空字串當成要執行的指令。
# 這個名字沒有任何 hook 讀它,只是佔位。
_extra="${6:-JSC_WRITE_GUARD_UNUSED=1}"
_out=$(env JSC_HOME="$wg_home" JSC_CLI="$cli" JSC_SESSION_ID=smoke-write \
JSC_SKILL="${4:-}" SKILL="${4:-}" JSC_TOOL_COMMAND="${5:-}" "$_extra" \
sh "$HOOKS/write-guard.sh" "$2" </dev/null 2>&1); _rc=$?
smoke_n_wg=$((smoke_n_wg + 1))
if [ "$_rc" -eq "$3" ]; then
printf '[jsc] write-guard.sh %s(%s):exit %s,與預期相同。\n' "$2" "$1" "$_rc" >> "$smoke_out"
else
smoke_fails=$((smoke_fails + 1))
printf '[jsc] write-guard.sh %s(%s):exit %s,預期 %s,寫入閘門判定壞了:%s\n' \
"$2" "$1" "$_rc" "$3" "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
fi
}
if wg_home=$(mktemp -d 2>/dev/null) && mkdir -p "$wg_home/sessions" 2>/dev/null; then
wg_state="$wg_home/sessions/smoke-write.stage"
wg_last="$wg_home/sessions/smoke-write.lastskill"
# stage:階段鎖狀態檔的格式沿用 sdlc-gate.sh 那一份,這裡照樣寫三欄。
smoke_wg_case "沒有階段鎖" stage 0
printf 'plan\treasoning-max\tsmoke\n' > "$wg_state"
smoke_wg_case "階段鎖 plan" stage 2
printf 'analyze\treasoning-max\tsmoke\n' > "$wg_state"
smoke_wg_case "階段鎖 analyze" stage 2
printf 'implement\tcoding\tsmoke\n' > "$wg_state"
smoke_wg_case "階段鎖 implement" stage 0
printf 'plan\treasoning-max\tsmoke\n' > "$wg_state"
smoke_wg_case "逃生門 JSC_WRITE_GUARD=off" stage 0 "" "" JSC_WRITE_GUARD=off
rm -f "$wg_state"
# review:技能名先看環境變數,取不到才讀 skill-usage.sh 記下的那一份,所以兩條來源都要驗。
smoke_wg_case "沒有技能紀錄" review 0
smoke_wg_case "jsc-review:code-review 執行中" review 2 jsc-review:code-review
smoke_wg_case "jsc-review:api-doc 執行中" review 2 jsc-review:api-doc
smoke_wg_case "jsc-review:comment-cleanup 本來就要寫檔" review 0 jsc-review:comment-cleanup
smoke_wg_case "其他技能 jsc-sdlc:implement" review 0 jsc-sdlc:implement
printf 'jsc-review:code-review' > "$wg_last"
smoke_wg_case "技能紀錄讀自 skill-usage.sh 那一份" review 2
smoke_wg_case "技能紀錄已過期" review 0 "" "" JSC_WRITE_GUARD_TTL=0
# release:稽核技能收尾時自己按的解除鍵。驗它自己跑得完、清完之後 review 真的不再擋、
# 以及紀錄本來就不在時照樣算成功——收尾呼叫可能被重跑,第二次失敗只會讓呼叫端誤判。
smoke_wg_case "release 清掉技能紀錄" release 0
smoke_wg_case "release 之後 review 不再擋" review 0
smoke_wg_case "release 冪等,紀錄不存在也算成功" release 0
rm -f "$wg_last"
# commit:指令改由 JSC_TOOL_COMMAND 餵,不必為了測試去拼一份 stdin JSON。
smoke_wg_case "不是 git 指令" commit 0 "" "ls -al"
smoke_wg_case "git add -A 後接單次提交" commit 2 "" "git add -A && git commit -m 修正"
smoke_wg_case "只有 git add -A,沒有提交" commit 0 "" "git add -A"
# 測試用的簡體字以八進位位元組組出來,不在原始碼裡留簡體字面:留了的話 lang-guard.sh
# 每次掃到這一行都會命中自己的測試資料,訊號會被自己的噪音蓋掉。
wg_bad=$(printf '\345\244\215\351\227\256')
smoke_wg_case "提交訊息含簡體字" commit 2 "" "git commit -m \"$wg_bad\""
smoke_wg_case "提交訊息為繁體中文" commit 0 "" "git commit -m \"修正問題\""
smoke_wg_case "逃生門 JSC_WRITE_GUARD=off" commit 0 "" "git add -A && git commit -m x" JSC_WRITE_GUARD=off
rm -rf "$wg_home"
else
smoke_fails=$((smoke_fails + 1))
printf '[jsc] write-guard.sh:建不出暫存目錄,寫入與提交閘門判定沒驗到。\n' >> "$smoke_out"
fi
# 自我斷言:實際跑過的條數對上宣告的預期條數,再對上真正印出來的行數。三邊一致才算數,
# 少跑一條或多印一行都會在這裡現形,散文就不必再自己記一份數字。
smoke_lines=$(wc -l < "$smoke_out" 2>/dev/null | tr -d ' ')
[ -n "$smoke_lines" ] || smoke_lines=0
smoke_total=$((SMOKE_EXPECT_HOOK + SMOKE_EXPECT_WP + SMOKE_EXPECT_RS + SMOKE_EXPECT_WG))
smoke_mismatch=""
[ "$smoke_n_hook" = "$SMOKE_EXPECT_HOOK" ] \
|| smoke_mismatch="${smoke_mismatch}hook 模式 $smoke_n_hook 條(預期 $SMOKE_EXPECT_HOOK);"
[ "$smoke_n_wp" = "$SMOKE_EXPECT_WP" ] \
|| smoke_mismatch="${smoke_mismatch}工作包 $smoke_n_wp 條(預期 $SMOKE_EXPECT_WP);"
[ "$smoke_n_rs" = "$SMOKE_EXPECT_RS" ] \
|| smoke_mismatch="${smoke_mismatch}重啟閘門 $smoke_n_rs 條(預期 $SMOKE_EXPECT_RS);"
[ "$smoke_n_wg" = "$SMOKE_EXPECT_WG" ] \
|| smoke_mismatch="${smoke_mismatch}寫入閘門 $smoke_n_wg 條(預期 $SMOKE_EXPECT_WG);"
[ "$smoke_lines" = "$smoke_total" ] \
|| smoke_mismatch="${smoke_mismatch}結果行數 $smoke_lines 行(預期 $smoke_total);"
smoke_breakdown="hook 模式 $SMOKE_EXPECT_HOOK 條、工作包 $SMOKE_EXPECT_WP 條、重啟閘門 $SMOKE_EXPECT_RS 條、寫入閘門 $SMOKE_EXPECT_WG 條"
if [ -n "$smoke_mismatch" ]; then
printf 'status=failed reason=%s\n' "冒煙結果行數與預期不符:${smoke_mismatch}判定路徑有增減時要一併改腳本裡的預期條數"
printf 'lines\t%s\n' "$smoke_lines"
cat "$smoke_out"
rm -f "$smoke_out"
echo "[jsc] 請先以 tools/report-error.sh 回報,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2
exit 4
fi
if [ "$smoke_fails" -eq 0 ]; then if [ "$smoke_fails" -eq 0 ]; then
printf 'status=ok reason=%s\n' "八支 hook 的每個接線模式都跑得完,工作包歸屬判定與部署後重啟閘門的每條路徑也各走過一次,沒有執行期錯誤" printf 'status=ok reason=%s\n' "九支 hook 的每個接線模式都跑得完,工作包歸屬、部署後重啟閘門與寫入提交閘門的每條路徑也各走過一次($smoke_breakdown),沒有執行期錯誤"
printf 'lines\t%s\n' "$smoke_lines"
cat "$smoke_out"; rm -f "$smoke_out"; exit 0 cat "$smoke_out"; rm -f "$smoke_out"; exit 0
fi fi
printf 'status=failed reason=%s\n' "$smoke_fails 支 hook 有執行期錯誤" printf 'status=failed reason=%s\n' "$smoke_fails 條判定有執行期錯誤"
printf 'lines\t%s\n' "$smoke_lines"
cat "$smoke_out" cat "$smoke_out"
rm -f "$smoke_out" rm -f "$smoke_out"
echo "[jsc] 請先以 tools/report-error.sh 回報,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2 echo "[jsc] 請先以 tools/report-error.sh 回報,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2
@@ -996,9 +1145,9 @@ if [ "$action" = status ]; then
claude_hooks="$claude_root/hooks/hooks.json" claude_hooks="$claude_root/hooks/hooks.json"
if [ -n "$claude_root" ] && [ -f "$claude_hooks" ]; then st_item hooks.json "$claude_hooks" present if [ -n "$claude_root" ] && [ -f "$claude_hooks" ]; then st_item hooks.json "$claude_hooks" present
else st_item hooks.json "${claude_hooks:-$HOME/.claude/plugins/installed_plugins.json}" missing; fi else st_item hooks.json "${claude_hooks:-$HOME/.claude/plugins/installed_plugins.json}" missing; fi
# 八支 hook 全靠這一個檔宣告,只看檔案在不在會漏掉「檔在、某支沒接進去」。 # 九支 hook 全靠這一個檔宣告,只看檔案在不在會漏掉「檔在、某支沒接進去」。
# 後來才加進來的 comment-scope.sh、lang-guard.sh 與 restart-gate.sh 是最可能漏的三支, # 後來才加進來的 comment-scope.sh、lang-guard.sh、restart-gate.sh 與 write-guard.sh
# 所以各列一項。 # 是最可能漏的四支,所以各列一項。
if [ -f "$claude_hooks" ] && grep -qF 'comment-scope.sh' "$claude_hooks" 2>/dev/null if [ -f "$claude_hooks" ] && grep -qF 'comment-scope.sh' "$claude_hooks" 2>/dev/null
then st_item comment-scope "$claude_hooks" present then st_item comment-scope "$claude_hooks" present
else st_item comment-scope "$claude_hooks" missing; fi else st_item comment-scope "$claude_hooks" missing; fi
@@ -1007,7 +1156,13 @@ if [ "$action" = status ]; then
else st_item lang-guard "$claude_hooks" missing; fi else st_item lang-guard "$claude_hooks" missing; fi
if [ -f "$claude_hooks" ] && grep -qF 'restart-gate.sh' "$claude_hooks" 2>/dev/null if [ -f "$claude_hooks" ] && grep -qF 'restart-gate.sh' "$claude_hooks" 2>/dev/null
then st_item restart-gate "$claude_hooks" present then st_item restart-gate "$claude_hooks" present
else st_item restart-gate "$claude_hooks" missing; fi ;; else st_item restart-gate "$claude_hooks" missing; fi
# write-guard.sh 接在兩個 matcher 上,三種模式各自是一件事,只驗腳本名會漏掉少接的那一個
for _m in stage review commit; do
if [ -f "$claude_hooks" ] && grep -qF "write-guard.sh\\\" $_m" "$claude_hooks" 2>/dev/null
then st_item "write-guard-$_m" "$claude_hooks" present
else st_item "write-guard-$_m" "$claude_hooks" missing; fi
done ;;
codex) codex)
config="${CODEX_HOME:-$HOME/.codex}/config.toml" config="${CODEX_HOME:-$HOME/.codex}/config.toml"
@@ -1044,7 +1199,7 @@ if [ "$action" = status ]; then
else else
st_item plugin-user-prompt-root "$_codex_plugin_hooks" missing st_item plugin-user-prompt-root "$_codex_plugin_hooks" missing
fi fi
st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時" ;; st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時" ;;
copilot) copilot)
st_rc_alias alias jsc-hooks:copilot st_rc_alias alias jsc-hooks:copilot
@@ -1052,7 +1207,7 @@ if [ "$action" = status ]; then
st_comment_scope comment-scope "${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" st_comment_scope comment-scope "${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}"
st_lang_guard lang-guard "${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" st_lang_guard lang-guard "${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}"
st_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:copilot" "# /jsc-hooks:copilot" st_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:copilot" "# /jsc-hooks:copilot"
st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;; st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;;
antigravity) antigravity)
st_rc_alias alias jsc-hooks:antigravity st_rc_alias alias jsc-hooks:antigravity
@@ -1060,7 +1215,7 @@ if [ "$action" = status ]; then
st_comment_scope comment-scope "${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}" st_comment_scope comment-scope "${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}"
st_lang_guard lang-guard "${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}" st_lang_guard lang-guard "${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}"
st_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:antigravity" "# /jsc-hooks:antigravity" st_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:antigravity" "# /jsc-hooks:antigravity"
st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;; st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;;
kiro) kiro)
# kiro 的 hook 檔綁在工作區,這裡看的一律是目前工作目錄底下那一份 # kiro 的 hook 檔綁在工作區,這裡看的一律是目前工作目錄底下那一份
@@ -1089,7 +1244,7 @@ if [ "$action" = status ]; then
else st_item lang-guard-sweep ./.kiro/hooks/jsc-hooks.json missing; fi else st_item lang-guard-sweep ./.kiro/hooks/jsc-hooks.json missing; fi
st_runtime_paths session-runtime-paths ./.kiro/hooks/jsc-hooks-session-start.json st_runtime_paths session-runtime-paths ./.kiro/hooks/jsc-hooks-session-start.json
st_runtime_paths hook-runtime-paths ./.kiro/hooks/jsc-hooks.json st_runtime_paths hook-runtime-paths ./.kiro/hooks/jsc-hooks.json
st_degrade="SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪提示送出時掃整個工作區,不是逐檔即時" ;; st_degrade="SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼改為每輪提示送出時掃整個工作區,不是逐檔即時" ;;
esac esac
if [ "$st_missing" -gt 0 ]; then if [ "$st_missing" -gt 0 ]; then
@@ -1100,7 +1255,7 @@ if [ "$action" = status ]; then
printf 'status=degraded reason=%s\n' "$st_degrade" printf 'status=degraded reason=%s\n' "$st_degrade"
cat "$st_items"; rm -f "$st_items"; exit 1 cat "$st_items"; rm -f "$st_items"; exit 1
fi fi
printf 'status=wired reason=%s\n' "hooks.json 自動接線全部八支 hook" printf 'status=wired reason=%s\n' "hooks.json 自動接線全部九支 hook"
cat "$st_items"; rm -f "$st_items"; exit 0 cat "$st_items"; rm -f "$st_items"; exit 0
fi fi
@@ -1118,10 +1273,14 @@ case "$cli" in
|| fail "$HOOKS/hooks.json 沒有接上 lang-guard.sh,繁中與編碼檢查不會生效" || fail "$HOOKS/hooks.json 沒有接上 lang-guard.sh,繁中與編碼檢查不會生效"
grep -qF 'restart-gate.sh' "$HOOKS/hooks.json" 2>/dev/null \ grep -qF 'restart-gate.sh' "$HOOKS/hooks.json" 2>/dev/null \
|| fail "$HOOKS/hooks.json 沒有接上 restart-gate.sh,部署後重啟閘門不會生效" || fail "$HOOKS/hooks.json 沒有接上 restart-gate.sh,部署後重啟閘門不會生效"
for m in stage review commit; do
grep -qF "write-guard.sh\\\" $m" "$HOOKS/hooks.json" 2>/dev/null \
|| fail "$HOOKS/hooks.json 沒有接上 write-guard.sh $m,這個模式不會生效"
done
printf 'status=wired reason=%s\n' "hooks.json 自動接線" printf 'status=wired reason=%s\n' "hooks.json 自動接線"
echo "$WIRE_PATH_NOTE" echo "$WIRE_PATH_NOTE"
echo "[jsc] claude:由 hooks/hooks.json 自動接線全部八支 hook,無需寫入設定。" echo "[jsc] claude:由 hooks/hooks.json 自動接線全部九支 hook,無需寫入設定。"
echo "[jsc] claude:只有 claude 有 pre-tool hook,版本前置檢查與部署後重啟閘門只在這裡擋得下技能呼叫;其他四個 CLI 兩道閘門都接不上。" echo "[jsc] claude:只有 claude 有 pre-tool hook,版本前置檢查、部署後重啟閘門與 write-guard.sh 的三種模式只在這裡擋得下來;其他四個 CLI 這幾道閘門都接不上。"
echo "[jsc] claude:只有 claude 有 post-tool hook,comment-scope.sh 與 lang-guard.sh 的逐檔即時掃描只在這裡接得上;其他四個 CLI 改用 sweep 掃整個工作區,時機晚一輪或晚到工作階段結束。" echo "[jsc] claude:只有 claude 有 post-tool hook,comment-scope.sh 與 lang-guard.sh 的逐檔即時掃描只在這裡接得上;其他四個 CLI 改用 sweep 掃整個工作區,時機晚一輪或晚到工作階段結束。"
exit 0 ;; exit 0 ;;
@@ -1159,7 +1318,7 @@ case "$cli" in
has_block "$agents" "<!-- jsc-hooks -->" || fail "$agents 寫入後讀不到 jsc-hooks 標記段落" has_block "$agents" "<!-- jsc-hooks -->" || fail "$agents 寫入後讀不到 jsc-hooks 標記段落"
has_comment_scope "$agents" || fail "$agents 寫入後讀不到註解範圍規則" has_comment_scope "$agents" || fail "$agents 寫入後讀不到註解範圍規則"
has_lang_guard "$agents" || fail "$agents 寫入後讀不到繁中與編碼規則" has_lang_guard "$agents" || fail "$agents 寫入後讀不到繁中與編碼規則"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時" printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時"
echo "$WIRE_PATH_NOTE" echo "$WIRE_PATH_NOTE"
echo "[jsc] codex:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh codex,啟動當下開始計時。" echo "[jsc] codex:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh codex,啟動當下開始計時。"
echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才生效。" echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才生效。"
@@ -1168,6 +1327,7 @@ case "$cli" in
echo "[jsc] codex:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" echo "[jsc] codex:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] codex:版本前置檢查接不上(codex 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" echo "[jsc] codex:版本前置檢查接不上(codex 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
echo "[jsc] codex:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。" echo "[jsc] codex:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。"
echo "[jsc] codex:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。"
echo "[jsc] codex:註解範圍與繁中編碼除了規則提示,每輪結束會由 notify 各掃一次整個 git 工作區(codex 沒有 post-tool hook,接不到逐檔即時掃描),回饋比 claude 晚一輪。" echo "[jsc] codex:註解範圍與繁中編碼除了規則提示,每輪結束會由 notify 各掃一次整個 git 工作區(codex 沒有 post-tool hook,接不到逐檔即時掃描),回饋比 claude 晚一輪。"
exit 1 ;; exit 1 ;;
@@ -1182,7 +1342,7 @@ case "$cli" in
has_block "$instr" "<!-- jsc-hooks -->" || fail "$instr 寫入後讀不到 jsc-hooks 標記段落" has_block "$instr" "<!-- jsc-hooks -->" || fail "$instr 寫入後讀不到 jsc-hooks 標記段落"
has_comment_scope "$instr" || fail "$instr 寫入後讀不到註解範圍規則" has_comment_scope "$instr" || fail "$instr 寫入後讀不到註解範圍規則"
has_lang_guard "$instr" || fail "$instr 寫入後讀不到繁中與編碼規則" has_lang_guard "$instr" || fail "$instr 寫入後讀不到繁中與編碼規則"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區"
echo "$WIRE_PATH_NOTE" echo "$WIRE_PATH_NOTE"
echo "[jsc] copilot:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh copilot。" echo "[jsc] copilot:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh copilot。"
echo "[jsc] copilot:已在 $instr 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。" echo "[jsc] copilot:已在 $instr 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。"
@@ -1190,6 +1350,7 @@ case "$cli" in
echo "[jsc] copilot:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" echo "[jsc] copilot:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] copilot:版本前置檢查接不上(copilot 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" echo "[jsc] copilot:版本前置檢查接不上(copilot 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
echo "[jsc] copilot:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。" echo "[jsc] copilot:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。"
echo "[jsc] copilot:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。"
echo "[jsc] copilot:註解範圍與繁中編碼除了規則提示,工作階段結束時由 jsc-wrap.sh 收尾各掃一次整個 git 工作區(copilot 連逐輪事件都沒有),回饋要等到離開 CLI 才看得到。" echo "[jsc] copilot:註解範圍與繁中編碼除了規則提示,工作階段結束時由 jsc-wrap.sh 收尾各掃一次整個 git 工作區(copilot 連逐輪事件都沒有),回饋要等到離開 CLI 才看得到。"
exit 1 ;; exit 1 ;;
@@ -1204,7 +1365,7 @@ case "$cli" in
has_block "$rules" "<!-- jsc-hooks -->" || fail "$rules 寫入後讀不到 jsc-hooks 標記段落" has_block "$rules" "<!-- jsc-hooks -->" || fail "$rules 寫入後讀不到 jsc-hooks 標記段落"
has_comment_scope "$rules" || fail "$rules 寫入後讀不到註解範圍規則" has_comment_scope "$rules" || fail "$rules 寫入後讀不到註解範圍規則"
has_lang_guard "$rules" || fail "$rules 寫入後讀不到繁中與編碼規則" has_lang_guard "$rules" || fail "$rules 寫入後讀不到繁中與編碼規則"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區"
echo "$WIRE_PATH_NOTE" echo "$WIRE_PATH_NOTE"
echo "[jsc] antigravity:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh antigravity。" echo "[jsc] antigravity:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh antigravity。"
echo "[jsc] antigravity:已在 $rules 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。" echo "[jsc] antigravity:已在 $rules 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。"
@@ -1212,6 +1373,7 @@ case "$cli" in
echo "[jsc] antigravity:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" echo "[jsc] antigravity:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] antigravity:版本前置檢查接不上(antigravity 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" echo "[jsc] antigravity:版本前置檢查接不上(antigravity 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
echo "[jsc] antigravity:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。" echo "[jsc] antigravity:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。"
echo "[jsc] antigravity:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。"
echo "[jsc] antigravity:註解範圍與繁中編碼除了規則提示,工作階段結束時由 jsc-wrap.sh 收尾各掃一次整個 git 工作區(antigravity 連逐輪事件都沒有),回饋要等到離開 CLI 才看得到。" echo "[jsc] antigravity:註解範圍與繁中編碼除了規則提示,工作階段結束時由 jsc-wrap.sh 收尾各掃一次整個 git 工作區(antigravity 連逐輪事件都沒有),回饋要等到離開 CLI 才看得到。"
exit 1 ;; exit 1 ;;
@@ -1264,12 +1426,13 @@ EOF
|| fail "$hookfile 的 run 沒有接到 lang-guard.sh prompt,每輪不會注入繁中與編碼規則" || fail "$hookfile 的 run 沒有接到 lang-guard.sh prompt,每輪不會注入繁中與編碼規則"
grep -qF 'lang-guard.sh\" sweep' "$hookfile" 2>/dev/null \ grep -qF 'lang-guard.sh\" sweep' "$hookfile" 2>/dev/null \
|| fail "$hookfile 的 run 沒有接到 lang-guard.sh sweep,kiro 每輪不會掃簡體字與亂碼" || fail "$hookfile 的 run 沒有接到 lang-guard.sh sweep,kiro 每輪不會掃簡體字與亂碼"
printf 'status=degraded reason=%s\n' "SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪提示送出時掃整個工作區,不是逐檔即時" printf 'status=degraded reason=%s\n' "SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查、部署後重啟閘門與寫入提交閘門,註解範圍與繁中編碼改為每輪提示送出時掃整個工作區,不是逐檔即時"
echo "$WIRE_PATH_NOTE" echo "$WIRE_PATH_NOTE"
echo "[jsc] kiro:已建立 $startfile(sessionStart 開始計時)與 $hookfile(JSC_CLI=kiro),兩份都已驗證。" echo "[jsc] kiro:已建立 $startfile(sessionStart 開始計時)與 $hookfile(JSC_CLI=kiro),兩份都已驗證。"
echo "[jsc] kiro:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。" echo "[jsc] kiro:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] kiro:版本前置檢查接不上(kiro 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。" echo "[jsc] kiro:版本前置檢查接不上(kiro 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
echo "[jsc] kiro:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。" echo "[jsc] kiro:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。"
echo "[jsc] kiro:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。"
echo "[jsc] kiro:註解範圍與繁中編碼除了規則提示,每輪提示送出時會各掃一次整個 git 工作區(kiro 沒有 post-tool hook),掃到的是上一輪寫的檔。" echo "[jsc] kiro:註解範圍與繁中編碼除了規則提示,每輪提示送出時會各掃一次整個 git 工作區(kiro 沒有 post-tool hook),掃到的是上一輪寫的檔。"
exit 1 ;; exit 1 ;;
esac esac