fix(hooks-install): 使用穩定路徑寫入 hook 接線

This commit is contained in:
2026-08-28 11:22:08 +08:00
parent de6b012356
commit 4e23e49159
3 changed files with 131 additions and 25 deletions
+123 -20
View File
@@ -13,7 +13,7 @@
# smoke 在接線之後跑,補上接線驗證看不到的那一半:接線只證明設定寫對位置,證不了 hook
# 跑起來不出錯(缺 node、路徑錯、權限不足都只在真的執行時才現形)。
# 有分支的判定另外驗結果,不只驗跑得完:sdlc-gate.sh 的工作包歸屬比對會自備一份暫時的
# $JSC_HOME 狀態檔,把「查無歸屬」「自己的工作包」「別的工作包」與逃生門各跑一次,比對結束碼。
# $JSC_HOME 狀態檔,把「查無歸屬」「自己的工作包」「別的工作包的阻擋與放行」與逃生門各跑一次,比對結束碼。
# restart-gate.sh 的部署後重啟閘門同法:當前 CLI 沒有狀態檔、只有別支 CLI 有狀態檔、當前
# CLI 那份存在、豁免技能、逃生門、取不到技能名各跑一次,再驗狀態檔本身在不在——清除只刪自己
# 那一份、別支那一份留著、舊格式的單一狀態檔照樣擋得下來且清得掉。
@@ -77,6 +77,11 @@ set -u
HERE=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$HERE/.." && pwd)
HOOKS="$ROOT/hooks"
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
WIRE_ROOT="$ROOT"
WIRE_HOOKS="$HOOKS"
WIRE_TOOLS="$HERE"
WIRE_PATH_NOTE=""
# cli_bin(CLI 代號 → 實際執行檔)的唯一來源在 lib.sh,包裝啟動器也用同一份
. "$HOOKS/lib.sh"
@@ -111,6 +116,61 @@ lang_guard_text() { sh "$HOOKS/lang-guard.sh" prompt 2>/dev/null | sed '/^exit /
# 三段合在一個標記段落裡,purge 與重跑接線都是整段處理,不必各自再記一組標記。
rules_text() { ste100_text; comment_scope_text; lang_guard_text; }
# 寫入 CLI 設定時用版本無關的穩定路徑。執行中的腳本仍從自己的 repo 讀規則,避免開發中
# 的檔案和剛建立的連結互相踩到;只有寫進外部設定的命令改走 current 連結。
ensure_stable_root() {
_link="$JSC_HOME/current/jsc-hooks"
_parent=$(dirname "$_link")
if mkdir -p "$_parent" 2>/dev/null &&
{ [ ! -e "$_link" ] || [ -L "$_link" ]; } &&
ln -sfn "$ROOT" "$_link" 2>/dev/null &&
[ -f "$_link/hooks/session-timer.sh" ] &&
[ -f "$_link/tools/jsc-wrap.sh" ]; then
WIRE_ROOT="$_link"
WIRE_HOOKS="$_link/hooks"
WIRE_TOOLS="$_link/tools"
WIRE_PATH_NOTE="[jsc] 接線命令使用穩定路徑 $_link,已指向 $ROOT。"
return 0
fi
WIRE_ROOT="$ROOT"
WIRE_HOOKS="$HOOKS"
WIRE_TOOLS="$HERE"
WIRE_PATH_NOTE="[jsc] 無法建立穩定路徑 $_link,已明確退回目前路徑 $ROOT;下次 plugin 換版後請重新接線。"
return 0
}
verify_wire_script() {
[ -f "$1" ] || fail "接線路徑不存在:$1"
}
verify_wire_root() {
verify_wire_script "$WIRE_HOOKS/session-timer.sh"
verify_wire_script "$WIRE_HOOKS/comment-scope.sh"
verify_wire_script "$WIRE_HOOKS/lang-guard.sh"
verify_wire_script "$WIRE_TOOLS/jsc-wrap.sh"
}
# 讀 Claude Code 真正載入的 jsc-hooks installPath。status claude 要看載入路徑,不看
# 目前這支 wire-cli.sh 所在位置,否則用開發 repo 跑 status 會蓋掉壞掉的安裝快取。
claude_loaded_root() {
_reg="$HOME/.claude/plugins/installed_plugins.json"
[ -f "$_reg" ] && [ -r "$_reg" ] || return 1
_seg=$(tr -d '\n' < "$_reg" \
| sed -n 's/.*"jsc-hooks@jsc"[[:space:]]*:[[:space:]]*\[\([^]]*\)\].*/\1/p')
[ -n "$_seg" ] || return 1
printf '%s' "$_seg" | tr ',' '\n' \
| sed -n 's/.*"installPath"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1
}
bad_runtime_path() {
case "$1" in
/root/plugins/hooks/*) return 0 ;;
*/cache/*/jsc-hooks/[0-9]*/*) return 0 ;;
esac
[ -e "$1" ] || return 0
return 1
}
# 驗證:規則檔真的收到註解範圍那一段了嗎。比對字串取自腳本的第一行實際輸出,
# 不是另外抄一句關鍵字——抄的關鍵字改腳本時不會跟著改,驗證就會永遠通過。
# $1=檔案
@@ -841,22 +901,51 @@ if [ "$action" = status ]; then
st_item "$1" "$HOME/.bashrc" missing
}
st_runtime_paths() {
_item="$1"
_file="$2"
[ -f "$_file" ] || { st_item "$_item" "$_file" missing; return 0; }
_scan="$_file"
_tmp=""
if [ -n "${3:-}" ]; then
_tmp=$(mktemp) || { st_item "$_item" "$_file" missing; return 0; }
awk -v bopen="$3" -v bshut="${4:-}" '
$0==bopen { take=1; print; next }
bshut != "" && $0==bshut { print; take=0; next }
take { print }
' "$_file" > "$_tmp"
_scan="$_tmp"
fi
_bad=""
_paths=$(grep -Eo "/[^\"' ;]+/(hooks|tools)/[^\"' ;]+\\.sh" "$_scan" 2>/dev/null || true)
for _p in $_paths; do
if bad_runtime_path "$_p"; then
_bad="$_p"
break
fi
done
[ -n "$_tmp" ] && rm -f "$_tmp"
[ -z "$_bad" ] && st_item "$_item" "$_file" present || st_item "$_item" "$_bad" missing
}
case "$cli" in
claude)
if [ -f "$HOOKS/hooks.json" ]; then st_item hooks.json "$HOOKS/hooks.json" present
else st_item hooks.json "$HOOKS/hooks.json" missing; fi
claude_root=$(claude_loaded_root || true)
claude_hooks="$claude_root/hooks/hooks.json"
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
# 八支 hook 全靠這一個檔宣告,只看檔案在不在會漏掉「檔在、某支沒接進去」。
# 後來才加進來的 comment-scope.sh、lang-guard.sh 與 restart-gate.sh 是最可能漏的三支,
# 所以各列一項。
if [ -f "$HOOKS/hooks.json" ] && grep -qF 'comment-scope.sh' "$HOOKS/hooks.json" 2>/dev/null
then st_item comment-scope "$HOOKS/hooks.json" present
else st_item comment-scope "$HOOKS/hooks.json" missing; fi
if [ -f "$HOOKS/hooks.json" ] && grep -qF 'lang-guard.sh' "$HOOKS/hooks.json" 2>/dev/null
then st_item lang-guard "$HOOKS/hooks.json" present
else st_item lang-guard "$HOOKS/hooks.json" missing; fi
if [ -f "$HOOKS/hooks.json" ] && grep -qF 'restart-gate.sh' "$HOOKS/hooks.json" 2>/dev/null
then st_item restart-gate "$HOOKS/hooks.json" present
else st_item restart-gate "$HOOKS/hooks.json" missing; fi ;;
if [ -f "$claude_hooks" ] && grep -qF 'comment-scope.sh' "$claude_hooks" 2>/dev/null
then st_item comment-scope "$claude_hooks" present
else st_item comment-scope "$claude_hooks" missing; fi
if [ -f "$claude_hooks" ] && grep -qF 'lang-guard.sh' "$claude_hooks" 2>/dev/null
then st_item lang-guard "$claude_hooks" present
else st_item lang-guard "$claude_hooks" missing; fi
if [ -f "$claude_hooks" ] && grep -qF 'restart-gate.sh' "$claude_hooks" 2>/dev/null
then st_item restart-gate "$claude_hooks" present
else st_item restart-gate "$claude_hooks" missing; fi ;;
codex)
config="${CODEX_HOME:-$HOME/.codex}/config.toml"
@@ -885,6 +974,8 @@ if [ "$action" = status ]; then
st_block ste100 "${CODEX_HOME:-$HOME/.codex}/AGENTS.md" "<!-- jsc-hooks -->"
st_comment_scope comment-scope "${CODEX_HOME:-$HOME/.codex}/AGENTS.md"
st_lang_guard lang-guard "${CODEX_HOME:-$HOME/.codex}/AGENTS.md"
st_runtime_paths runtime-paths "$config" "# jsc-hooks" "# /jsc-hooks"
st_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:codex" "# /jsc-hooks:codex"
st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時" ;;
copilot)
@@ -892,6 +983,7 @@ if [ "$action" = status ]; then
st_block ste100 "${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}" "<!-- jsc-hooks -->"
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_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:copilot" "# /jsc-hooks:copilot"
st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;;
antigravity)
@@ -899,6 +991,7 @@ if [ "$action" = status ]; then
st_block ste100 "${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}" "<!-- jsc-hooks -->"
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_runtime_paths alias-paths "$HOME/.bashrc" "# jsc-hooks:antigravity" "# /jsc-hooks:antigravity"
st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區" ;;
kiro)
@@ -926,6 +1019,8 @@ if [ "$action" = status ]; then
grep -qF 'lang-guard.sh\" sweep' ./.kiro/hooks/jsc-hooks.json 2>/dev/null
then st_item lang-guard-sweep ./.kiro/hooks/jsc-hooks.json present
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 hook-runtime-paths ./.kiro/hooks/jsc-hooks.json
st_degrade="SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪提示送出時掃整個工作區,不是逐檔即時" ;;
esac
@@ -941,6 +1036,9 @@ if [ "$action" = status ]; then
cat "$st_items"; rm -f "$st_items"; exit 0
fi
ensure_stable_root
verify_wire_root
case "$cli" in
claude)
bin=$(cli_bin claude)
@@ -953,6 +1051,7 @@ case "$cli" in
grep -qF 'restart-gate.sh' "$HOOKS/hooks.json" 2>/dev/null \
|| fail "$HOOKS/hooks.json 沒有接上 restart-gate.sh,部署後重啟閘門不會生效"
printf 'status=wired reason=%s\n' "hooks.json 自動接線"
echo "$WIRE_PATH_NOTE"
echo "[jsc] claude:由 hooks/hooks.json 自動接線全部八支 hook,無需寫入設定。"
echo "[jsc] claude:只有 claude 有 pre-tool hook,版本前置檢查與部署後重啟閘門只在這裡擋得下技能呼叫;其他四個 CLI 兩道閘門都接不上。"
echo "[jsc] claude:只有 claude 有 post-tool hook,comment-scope.sh 與 lang-guard.sh 的逐檔即時掃描只在這裡接得上;其他四個 CLI 改用 sweep 掃整個工作區,時機晚一輪或晚到工作階段結束。"
@@ -972,11 +1071,11 @@ case "$cli" in
# notify 最後再掛 comment-scope.sh sweep:codex 沒有 post-tool hook,拿不到「剛剛寫了
# 哪個檔」,只能改掃整個 git 工作區的 diff。每輪結束掃一次,時機比 claude 晚,那一輪
# 寫過的檔一個都不會漏。
timer="sh '$HOOKS/session-timer.sh'"
scope="sh '$HOOKS/comment-scope.sh'"
lang="sh '$HOOKS/lang-guard.sh'"
timer="sh '$WIRE_HOOKS/session-timer.sh'"
scope="sh '$WIRE_HOOKS/comment-scope.sh'"
lang="sh '$WIRE_HOOKS/lang-guard.sh'"
notify_line="notify = [\"env\", \"JSC_CLI=codex\", \"sh\", \"-c\", \"$timer start </dev/null; $timer mark </dev/null; $scope sweep </dev/null; $lang sweep </dev/null\"]"
alias_line="alias $bin='sh \"$HERE/jsc-wrap.sh\" codex'"
alias_line="alias $bin='sh \"$WIRE_TOOLS/jsc-wrap.sh\" codex'"
replace_block_toml "$config" "# jsc-hooks" "# /jsc-hooks" "$notify_line" \
|| fail "無法寫入 $config"
has_block "$config" "# jsc-hooks" || fail "$config 寫入後讀不到 jsc-hooks 標記段落"
@@ -993,6 +1092,7 @@ case "$cli" in
has_comment_scope "$agents" || fail "$agents 寫入後讀不到註解範圍規則"
has_lang_guard "$agents" || fail "$agents 寫入後讀不到繁中與編碼規則"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時"
echo "$WIRE_PATH_NOTE"
echo "[jsc] codex:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh codex,啟動當下開始計時。"
echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才生效。"
echo "[jsc] codex:已設定 $config 的 notify(根層鍵,已驗證),每輪補 session-timer.sh start 再 mark,最後跑 comment-scope.sh sweep 與 lang-guard.sh sweep。"
@@ -1007,7 +1107,7 @@ case "$cli" in
bin=$(cli_bin copilot)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 copilot 執行檔"
instr="${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}"
alias_line="alias $bin='sh \"$HERE/jsc-wrap.sh\" copilot'"
alias_line="alias $bin='sh \"$WIRE_TOOLS/jsc-wrap.sh\" copilot'"
write_alias_rc "jsc-hooks:copilot" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔"
replace_block "$instr" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" "$(rules_text)" \
|| fail "無法寫入 $instr"
@@ -1015,6 +1115,7 @@ case "$cli" in
has_comment_scope "$instr" || fail "$instr 寫入後讀不到註解範圍規則"
has_lang_guard "$instr" || fail "$instr 寫入後讀不到繁中與編碼規則"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區"
echo "$WIRE_PATH_NOTE"
echo "[jsc] copilot:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh copilot。"
echo "[jsc] copilot:已在 $instr 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。"
echo "[jsc] copilot:別名要開新的 shell 或重新 source rc 檔才生效。"
@@ -1028,7 +1129,7 @@ case "$cli" in
bin=$(cli_bin antigravity)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 antigravity(agy)執行檔"
rules="${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}"
alias_line="alias $bin='sh \"$HERE/jsc-wrap.sh\" antigravity'"
alias_line="alias $bin='sh \"$WIRE_TOOLS/jsc-wrap.sh\" antigravity'"
write_alias_rc "jsc-hooks:antigravity" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔"
replace_block "$rules" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" "$(rules_text)" \
|| fail "無法寫入 $rules"
@@ -1036,6 +1137,7 @@ case "$cli" in
has_comment_scope "$rules" || fail "$rules 寫入後讀不到註解範圍規則"
has_lang_guard "$rules" || fail "$rules 寫入後讀不到繁中與編碼規則"
printf 'status=degraded reason=%s\n' "STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼只在工作階段結束時掃一次整個工作區"
echo "$WIRE_PATH_NOTE"
echo "[jsc] antigravity:已在 shell rc 加上 $bin 別名,轉呼叫 tools/jsc-wrap.sh antigravity。"
echo "[jsc] antigravity:已在 $rules 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。"
echo "[jsc] antigravity:別名要開新的 shell 或重新 source rc 檔才生效。"
@@ -1060,7 +1162,7 @@ case "$cli" in
"description": "jsc session timer start (auto-generated by jsc-hooks:hooks-install, do not edit by hand)",
"on": ["sessionStart"],
"env": { "JSC_CLI": "kiro" },
"run": "sh \\"$HOOKS/session-timer.sh\\" restart </dev/null"
"run": "sh \\"$WIRE_HOOKS/session-timer.sh\\" restart </dev/null"
}
EOF
# userPromptSubmit 那一輪的 run 最後再掛 comment-scope.sh sweep 與 lang-guard.sh sweep:
@@ -1072,7 +1174,7 @@ EOF
"description": "jsc session timer + STE100 guard + comment scope + language guard bridge (auto-generated by jsc-hooks:hooks-install, do not edit by hand)",
"on": ["sessionEnd", "userPromptSubmit"],
"env": { "JSC_CLI": "kiro" },
"run": "sh \\"$HOOKS/session-timer.sh\\" mark </dev/null; sh \\"$HOOKS/ste100-guard.sh\\" </dev/null; sh \\"$HOOKS/comment-scope.sh\\" prompt </dev/null; sh \\"$HOOKS/comment-scope.sh\\" sweep </dev/null; sh \\"$HOOKS/lang-guard.sh\\" prompt </dev/null; sh \\"$HOOKS/lang-guard.sh\\" sweep </dev/null"
"run": "sh \\"$WIRE_HOOKS/session-timer.sh\\" mark </dev/null; sh \\"$WIRE_HOOKS/ste100-guard.sh\\" </dev/null; sh \\"$WIRE_HOOKS/comment-scope.sh\\" prompt </dev/null; sh \\"$WIRE_HOOKS/comment-scope.sh\\" sweep </dev/null; sh \\"$WIRE_HOOKS/lang-guard.sh\\" prompt </dev/null; sh \\"$WIRE_HOOKS/lang-guard.sh\\" sweep </dev/null"
}
EOF
for f in "$startfile" "$hookfile"; do
@@ -1095,6 +1197,7 @@ EOF
grep -qF 'lang-guard.sh\" sweep' "$hookfile" 2>/dev/null \
|| fail "$hookfile 的 run 沒有接到 lang-guard.sh sweep,kiro 每輪不會掃簡體字與亂碼"
printf 'status=degraded reason=%s\n' "SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪提示送出時掃整個工作區,不是逐檔即時"
echo "$WIRE_PATH_NOTE"
echo "[jsc] kiro:已建立 $startfile(sessionStart 開始計時)與 $hookfile(JSC_CLI=kiro),兩份都已驗證。"
echo "[jsc] kiro:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] kiro:版本前置檢查接不上(kiro 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"