Files
hooks/tools/wire-cli.sh
T

1236 lines
69 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# wire-cli.sh — 單一 CLI 的 hook 生命週期:先清、再接、再冒煙(供 hooks-install 技能呼叫)。
# 用法:
# wire-cli.sh {claude|codex|copilot|antigravity|kiro} 接線
# wire-cli.sh purge {claude|codex|copilot|antigravity|kiro} 備份後移除該 CLI 的所有 hook
# wire-cli.sh smoke {claude|codex|copilot|antigravity|kiro} 跑一輪八支 hook,驗執行期
# wire-cli.sh status {claude|codex|copilot|antigravity|kiro} 唯讀盤點接線現況,不寫檔也不執行 hook
#
# purge 移除的是「所有 hook」,含非 jsc 的第三方項目。安裝一律先 purge 再接線:混著別人的
# hook 接線,出錯時分不清是誰的 hook 壞掉,也修不了。移除前每個要動的檔案先原樣複製到
# $JSC_HOME/backup/hooks/{cli}/{yyyyMMdd_HHmmss}/,備份失敗就不移除。
#
# smoke 在接線之後跑,補上接線驗證看不到的那一半:接線只證明設定寫對位置,證不了 hook
# 跑起來不出錯(缺 node、路徑錯、權限不足都只在真的執行時才現形)。
# 有分支的判定另外驗結果,不只驗跑得完:sdlc-gate.sh 的工作包歸屬比對會自備一份暫時的
# $JSC_HOME 狀態檔,把「查無歸屬」「自己的工作包」「別的工作包的阻擋與放行」與逃生門各跑一次,比對結束碼。
# restart-gate.sh 的部署後重啟閘門同法:當前 CLI 沒有狀態檔、只有別支 CLI 有狀態檔、當前
# CLI 那份存在、豁免技能、逃生門、取不到技能名各跑一次,再驗狀態檔本身在不在——清除只刪自己
# 那一份、別支那一份留著、舊格式的單一狀態檔照樣擋得下來且清得掉。
#
# 接線行為(依 CLI 而定,皆為冪等:重跑只取代既有的 jsc-hooks 標記段落,不會重複疊加):
# claude — 什麼都不用寫,hooks.json 已自動接線八支 hook
# codex — 在 shell rc 檔加上 codex 別名,轉呼叫 tools/jsc-wrap.sh codex(開始計時,
# 結束時收尾掃一次註解範圍與繁中編碼);在 config.toml 設 notify(每輪補
# session-timer.sh start 再 mark,最後 comment-scope.sh sweep 與
# lang-guard.sh sweep 掃整個工作區,JSC_CLI=codex);在 AGENTS.md 附加
# STE100、註解範圍與繁中編碼規則段落(prompt 降級)
# copilot — 在 shell rc 檔加上 copilot 別名,轉呼叫 tools/jsc-wrap.sh copilot(結束時
# 收尾掃一次註解範圍與繁中編碼);在 copilot-instructions.md 附加 STE100、
# 註解範圍與繁中編碼規則段落
# antigravity — 在 shell rc 檔加上 agy 別名,轉呼叫 tools/jsc-wrap.sh antigravity(同樣收尾
# 掃一次);在全域規則檔附加 STE100、註解範圍與繁中編碼規則段落
# kiro — 在工作區 .kiro/hooks/ 下建立 jsc-hooks.json(每輪 mark 加 STE100、
# 註解範圍與繁中編碼規則,再 comment-scope.sh sweep 與 lang-guard.sh sweep
# 掃整個工作區)與 jsc-hooks-session-start.json(sessionStart 開始計時),
# 皆帶 JSC_CLI=kiro
#
# 覆蓋範圍要據實回報,不得暗示每個 CLI 都有保護:
# claude 八支 hook 全接,回報 wired
# codex、copilot、antigravity、kiro 只有別名、notify 或規則檔,接不上 PreToolUse、PostToolUse
# 與 UserPromptSubmit,版本前置檢查、部署後重啟閘門與
# SDLC 模型鎖都沒接上,一律回報 degraded 並在 reason 講明。
# 重啟閘門在這四個 CLI 上一次技能呼叫都擋不下來:狀態檔照樣
# 寫、下次工作階段開始照樣清,只是中間沒有任何判定點,重啟
# 只能靠 /jsc-cli:deploy 收尾的提示自己動手
#
# 註解範圍與繁中編碼掃描每個 CLI 的時機都不同(兩支腳本接在同一批位置),回報時不得寫成五支一樣:
# claude 掛在 PostToolUse,寫完哪個檔就掃哪個,逐檔即時
# codex 掛在每輪結束的 notify,掃整個 git 工作區這輪改過的檔(sweep)
# kiro 掛在 userPromptSubmit,掃整個 git 工作區,掃到的是上一輪寫的檔
# copilot、antigravity 沒有任何逐輪事件,只有工作階段結束時由 jsc-wrap.sh 收尾掃一次
# 時機晚一點、涵蓋範圍一樣:sweep 看的是 git diff,那一輪寫過的檔一個都不會漏。真正的差別
# 在回饋速度——claude 當下就叫,其他四個要等到該輪或該階段結束。lang-guard.sh 的三種模式
# 一律跟著 comment-scope.sh 接在同一批位置,兩支的掃描時機表完全一致。
# 所有 jsc 標記段落都採整段重寫,重跑等同先移除舊內容再重裝
#
# 寫入後自我驗證,通過才回報成功:每個寫過的檔案重新讀一次,確認標記段落存在且落在
# 正確位置(codex 的 notify 必須是根層鍵,不能被歸進前一張表;kiro 的 JSON 必須成對
# 且 on、run 在最上層),內容也要涵蓋這次該接上的每一支腳本(含 comment-scope.sh sweep
# 與 lang-guard.sh sweep)。
# 腳本說寫好了卻寫錯位置或少接一支,是最難查的失敗,所以驗證放在腳本裡。
#
# 輸出: 第一行固定為 `status=... reason=...`(可供程式判讀),其後為人類可讀的繁中說明。
# 結束碼(接線): 0=wired(已完整接線) 1=degraded(降級為 prompt/技能步驟檢查)
# 2=用法錯誤 3=skipped(該 CLI 未偵測到執行檔,略過)
# 4=failed(寫入或驗證沒過,接線沒生效;由 hooks-install 呼叫 report-error.sh 回報)
# 結束碼(purge): 0=purged 2=用法錯誤 3=skipped 4=failed
# 結束碼(smoke): 0=ok 2=用法錯誤 4=failed
# 結束碼(status): 0=wired 1=degraded 2=用法錯誤 3=skipped 5=unwired(該接的段落缺了至少一項)
# status 之外的動作都會寫檔,體檢類技能(/jsc-cli:doctor)只能呼叫 status。判讀邏輯跟接線
# 共用同一組檔案位置與標記字串,分兩份實作就會各自漂移,體檢說沒接、實際上接著。
#
# JSC_CLAUDE_SETTINGS_DIR 可覆寫 claude 使用者層設定檔目錄(預設 ~/.claude)。
# 有這個逃生門才測得動 purge 的 JSON 刪鍵:預設路徑是使用者自己的設定檔,拿真檔案試刪
# 等於拿使用者的環境當測試場。指向一份複製品就能完整跑過 purge claude 而不動到本人設定。
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"
usage() {
echo "用法:wire-cli.sh [purge|smoke|status] {claude|codex|copilot|antigravity|kiro}" >&2
exit 2
}
# 第一個參數是子命令時走新流程,否則沿用原本的「wire-cli.sh {cli}」接線。
action=wire
case "${1:-}" in
purge|smoke|status) action="$1"; shift ;;
esac
cli="${1:-}"
case "$cli" in
claude|codex|copilot|antigravity|kiro) ;;
*) usage ;;
esac
# STE100 規則段落的唯一來源:ste100-guard.sh 的實際輸出
ste100_text() { sh "$HOOKS/ste100-guard.sh" 2>/dev/null | sed '/^exit /d'; }
# 註解範圍規則段落的唯一來源:comment-scope.sh prompt 的實際輸出。
# 規則正文不在這裡抄一份:抄了就會跟腳本各自漂移,兩邊講的規則對不起來。
comment_scope_text() { sh "$HOOKS/comment-scope.sh" prompt 2>/dev/null | sed '/^exit /d'; }
# 繁中與編碼規則段落的唯一來源:lang-guard.sh prompt 的實際輸出。理由同上,不在這裡抄一份。
lang_guard_text() { sh "$HOOKS/lang-guard.sh" prompt 2>/dev/null | sed '/^exit /d'; }
# 寫進規則檔的完整段落:語言規則加註解範圍規則加繁中編碼規則,共用同一組 jsc-hooks 標記。
# 三段合在一個標記段落裡,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=檔案
has_comment_scope() {
_first=$(comment_scope_text | head -n1)
[ -n "$_first" ] || return 1
grep -qF "$_first" "$1" 2>/dev/null
}
# 驗證:規則檔真的收到繁中與編碼那一段了嗎。同樣取腳本的第一行實際輸出來比對,理由同上。
# $1=檔案
has_lang_guard() {
_first=$(lang_guard_text | head -n1)
[ -n "$_first" ] || return 1
grep -qF "$_first" "$1" 2>/dev/null
}
codex_plugin_hooks_json() {
_base="${CODEX_HOME:-$HOME/.codex}/plugins/cache/jsc/jsc-hooks"
[ -d "$_base" ] || return 1
find "$_base" -path '*/hooks/hooks.json' -type f 2>/dev/null | sort | tail -n1
}
codex_plugin_hooks_safe() {
_hooks_json=$(codex_plugin_hooks_json || true)
[ -n "$_hooks_json" ] || return 0
! grep -qF 'command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/' "$_hooks_json" 2>/dev/null
}
# 以標記整段取代(冪等);標記不存在就在檔尾新增;檔案不存在就建立。
# 適用 markdown 規則檔與 shell rc 檔:這兩種檔案沒有「區段」概念,附在檔尾就對了。
# $1=檔案 $2=開頭標記行 $3=結尾標記行 $4=標記之間要寫入的內容
replace_block() {
file="$1"; bopen="$2"; bshut="$3"; content="$4"
dir=$(dirname "$file")
mkdir -p "$dir" 2>/dev/null || return 1
touch "$file" 2>/dev/null || return 1
# touch 對目錄也會成功,所以要另外確認它真的是一般檔案;不然接著的寫入才失敗,
# 而 shell 開檔失敗的訊息蓋不掉,會漏一行 cannot create 給使用者看。
[ -f "$file" ] || return 1
block=$(printf '%s\n%s\n%s' "$bopen" "$content" "$bshut")
if grep -qF "$bopen" "$file" 2>/dev/null; then
awk -v bopen="$bopen" -v bshut="$bshut" -v block="$block" '
$0==bopen { print block; skip=1; next }
$0==bshut { skip=0; next }
skip { next }
{ print }
' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
else
# 包一層子 shell 才蓋得住 shell 自己的開檔失敗訊息(>> 失敗時那行不走命令的 stderr)
( printf '\n%s\n' "$block" >> "$file" ) 2>/dev/null || return 1
fi
}
# TOML 版的整段取代:標記段落一律放在第一個表頭(`[table]`、`[[array]]`)之前。
# TOML 的根層鍵只在第一個表頭之前有效,附在檔尾會被歸進最後那張表——檔案照樣解析
# 得過,codex 卻永遠讀不到 notify,hook 靜靜失效。所以位置本身就是正確性的一部分。
# 舊版寫錯位置的段落也會被這支函式移到正確位置(先整段刪除,再插到表頭之前)。
# $1=檔案 $2=開頭標記行 $3=結尾標記行 $4=標記之間要寫入的內容
replace_block_toml() {
file="$1"; bopen="$2"; bshut="$3"; content="$4"
dir=$(dirname "$file")
mkdir -p "$dir" 2>/dev/null || return 1
touch "$file" 2>/dev/null || return 1
# touch 對目錄也會成功,所以要另外確認它真的是一般檔案;不然接著的寫入才失敗,
# 而 shell 開檔失敗的訊息蓋不掉,會漏一行 cannot create 給使用者看。
[ -f "$file" ] || return 1
block=$(printf '%s\n%s\n%s' "$bopen" "$content" "$bshut")
awk -v bopen="$bopen" -v bshut="$bshut" -v block="$block" '
$0==bopen { skip=1; next }
$0==bshut { skip=0; next }
skip { next }
# 表頭樣式:整行只有 [name] 或 [[name]]。多行陣列裡的 [1, 2], 不會命中。
!done && /^[ \t]*\[\[?[^][]+\]\]?[ \t]*$/ { print block; print ""; done=1 }
{ print }
END { if (!done) print block }
' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
}
# 驗證:檔案裡有這段標記嗎($1=檔案 $2=開頭標記行)
has_block() { grep -qF "$2" "$1" 2>/dev/null; }
# 驗證:TOML 的某個鍵是不是落在根層(第一個表頭之前)。$1=檔案 $2=鍵名
toml_root_key() {
awk -v k="$2" '
/^[ \t]*\[\[?[^][]+\]\]?[ \t]*$/ { intable=1; next }
!intable && $0 ~ "^[ \t]*" k "[ \t]*=" { found=1 }
END { exit(found ? 0 : 1) }
' "$1" 2>/dev/null
}
# 驗證:JSON 括號成對,且某個鍵出現在最上層($1=檔案 $2=鍵名 $3=要求)。
# 解析失敗(括號不成對、字串沒收尾)也回傳非 0,所以這支同時當語法檢查用。
# $3=key(預設)要求該鍵存在;$3=pairs 只檢查語法,不管鍵在不在——purge 之後要驗的是
# 「鍵不見了而且檔案還是合法 JSON」,這兩件事得分開問,不然刪壞檔也會被當成刪成功。
json_top_key() {
awk -v k="$2" -v want="${3:-key}" '
{ s = s $0 "\n" }
END {
n = length(s); depth = 0; i = 1; found = 0; bad = 0
while (i <= n) {
c = substr(s, i, 1)
if (c == "\"") {
buf = ""; i++; closed = 0
while (i <= n) {
c = substr(s, i, 1)
if (c == "\\") { i += 2; continue }
if (c == "\"") { i++; closed = 1; break }
buf = buf c; i++
}
if (!closed) { bad = 1; break }
j = i
while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++
if (substr(s, j, 1) == ":" && depth == 1 && buf == k) found = 1
continue
}
if (c == "{" || c == "[") depth++
else if (c == "}" || c == "]") { depth--; if (depth < 0) { bad = 1; break } }
i++
}
if (bad || depth != 0) exit(1)
exit((want == "pairs" || found) ? 0 : 1)
}' "$1" 2>/dev/null
}
# 驗證:JSON 語法成對(括號收齊、字串收尾)。鍵不管。
json_pairs_ok() { json_top_key "$1" __no_such_key__ pairs; }
# 找出已存在的 shell rc 檔(purge 用)。rc_files 找不到會建立 ~/.bashrc,移除流程不建檔:
# 為了清 hook 而生出一個新檔案,是把環境弄得更亂,不是更乾淨。
rc_files_existing() {
for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do
[ -f "$f" ] && printf '%s\n' "$f"
done
return 0
}
# 找出可寫入別名的 shell rc 檔;都不存在就以 ~/.bashrc 為預設(自動建立)。
# 印出找到或建立的 rc 檔路徑,一行一個。
rc_files() {
found=""
for f in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do
[ -f "$f" ] && { printf '%s\n' "$f"; found=1; }
done
[ -n "$found" ] || printf '%s\n' "$HOME/.bashrc"
}
# 把別名寫進每個 rc 檔並逐檔驗證。$1=標記名(不含 # 與 /)$2=別名內容
# 迴圈不可以放在管線右邊:那會變成子 shell,寫入失敗的旗標傳不回來,
# 明明沒寫成功也照樣回報 wired。改成從暫存檔讀,迴圈就留在本 shell。
write_alias_rc() {
_mark="$1"; _line="$2"; _ok=1
_list=$(mktemp) || return 1
rc_files > "$_list" || { rm -f "$_list"; return 1; }
while IFS= read -r rc; do
[ -n "$rc" ] || continue
replace_block "$rc" "# $_mark" "# /$_mark" "$_line" || { _ok=0; continue; }
grep -qF "$_line" "$rc" 2>/dev/null || _ok=0
done < "$_list"
rm -f "$_list"
[ "$_ok" = 1 ]
}
# --- 移除(purge)用的函式 ---
# 以標記整段移除(冪等)。與 replace_block 對稱:同一組標記,一支寫入、一支移除。
# 標記不存在就當成已移除、回傳成功——purge 重跑不該因為「上次已經清掉了」而失敗。
# $1=檔案 $2=開頭標記行 $3=結尾標記行
remove_block() {
file="$1"; bopen="$2"; bshut="$3"
[ -f "$file" ] || return 0
grep -qF "$bopen" "$file" 2>/dev/null || return 0
awk -v bopen="$bopen" -v bshut="$bshut" '
function trim(s) {
sub(/^[ \t]+/, "", s)
sub(/[ \t]+$/, "", s)
return s
}
trim($0)==bopen { skip=1; next }
trim($0)==bshut { skip=0; next }
skip { next }
{ print }
' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
}
# 移除 rc 檔裡所有 `# jsc-hooks*` 標記段落,不管後面接哪個 CLI 名。
# 舊版接線可能留下已改名的段落,逐一指名會漏掉,所以用前綴一次掃乾淨。
# $1=檔案
remove_rc_blocks() {
file="$1"
[ -f "$file" ] || return 0
grep -q '^# jsc-hooks' "$file" 2>/dev/null || return 0
awk '
function trim(s) {
sub(/^[ \t]+/, "", s)
sub(/[ \t]+$/, "", s)
return s
}
trim($0) ~ /^# jsc-hooks/ { skip=1; next }
trim($0) ~ /^# \/jsc-hooks/ { skip=0; next }
skip { next }
{ print }
' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
}
# 移除 TOML 的根層鍵(第一個表頭之前的那個鍵),含非 jsc 設的值。
# 值可能是多行陣列或多行行內表,所以要追括號深度,收齊才停;只刪一行會留下孤兒括號。
# $1=檔案 $2=鍵名
remove_toml_root_key() {
file="$1"; key="$2"
[ -f "$file" ] || return 0
awk -v k="$key" '
BEGIN { intable=0; drop=0; depth=0 }
/^[ \t]*\[\[?[^][]+\]\]?[ \t]*$/ { intable=1 }
{
if (drop) {
depth += gsub(/[[{]/, "&") - gsub(/[]}]/, "&")
if (depth <= 0) drop=0
next
}
if (!intable && $0 ~ "^[ \t]*" k "[ \t]*=") {
depth = gsub(/[[{]/, "&") - gsub(/[]}]/, "&")
if (depth > 0) drop=1
next
}
print
}
' "$file" > "$file.jsc-tmp" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
mv "$file.jsc-tmp" "$file" 2>/dev/null || { rm -f "$file.jsc-tmp"; return 1; }
}
# 刪掉 JSON 最上層的 hooks 鍵(含後面多餘的逗號),逐字元追蹤引號與括號深度。
# jq 在目標機器上不保證存在,所以要有這條純 awk 的路;只用 sed 刪不了嵌套的 {...}。
# 追蹤引號是必要的:字串裡的 { 與 } 不算深度,漏算就會把整段設定切壞。
# $1=檔案,結果印到標準輸出;解析不出來(括號不成對、字串沒收尾)就 exit 1,不輸出半份檔案。
awk_del_hooks() {
awk '
function skip_string(s, i, n, c) {
i++
while (i <= n) {
c = substr(s, i, 1)
if (c == "\\") { i += 2; continue }
if (c == "\"") return i + 1
i++
}
return 0
}
function skip_value(s, i, n, c, d) {
while (i <= n && substr(s, i, 1) ~ /[ \t\r\n]/) i++
c = substr(s, i, 1)
if (c == "\"") return skip_string(s, i, n)
if (c == "{" || c == "[") {
d = 0
while (i <= n) {
c = substr(s, i, 1)
if (c == "\"") { i = skip_string(s, i, n); if (i == 0) return 0; continue }
if (c == "{" || c == "[") { d++; i++; continue }
if (c == "}" || c == "]") { d--; i++; if (d == 0) return i; continue }
i++
}
return 0
}
while (i <= n && substr(s, i, 1) !~ /[,}\]\t\r\n ]/) i++
return i
}
{ s = s $0 "\n" }
END {
n = length(s); i = 1; depth = 0; out = ""
while (i <= n) {
c = substr(s, i, 1)
if (c == "\"") {
start = i; buf = ""; j = i + 1
while (j <= n) {
c = substr(s, j, 1)
if (c == "\\") { j += 2; continue }
if (c == "\"") break
buf = buf c; j++
}
if (j > n) exit 1
i = j + 1
j = i
while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++
if (depth == 1 && buf == "hooks" && substr(s, j, 1) == ":") {
i = skip_value(s, j + 1, n)
if (i == 0) exit 1
j = i
while (j <= n && substr(s, j, 1) ~ /[ \t\r\n]/) j++
if (substr(s, j, 1) == ",") {
# 後面還有成員:連逗號一起吃掉,並把 hooks 那行留下的縮排收乾淨
i = j + 1
sub(/[ \t]+$/, "", out)
while (i <= n && substr(s, i, 1) ~ /[ \t\r]/) i++
if (substr(s, i, 1) == "\n") i++
} else {
# hooks 是最後一個成員:改刪前一個逗號,不刪會留下「, }」這種壞掉的 JSON
sub(/,[ \t\r\n]*$/, "", out)
}
continue
}
out = out substr(s, start, i - start)
continue
}
if (c == "{" || c == "[") depth++
else if (c == "}" || c == "]") { depth--; if (depth < 0) exit 1 }
out = out c; i++
}
if (depth != 0) exit 1
printf "%s", out
}' "$1"
}
# 刪掉設定檔最上層的 hooks 鍵:有 jq 就用 jq,沒有就走 awk_del_hooks。$1=檔案
json_del_hooks() {
_f="$1"
[ -f "$_f" ] || return 0
if command -v jq >/dev/null 2>&1; then
jq 'del(.hooks)' "$_f" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; }
else
awk_del_hooks "$_f" > "$_f.jsc-tmp" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; }
fi
[ -s "$_f.jsc-tmp" ] || { rm -f "$_f.jsc-tmp"; return 1; }
mv "$_f.jsc-tmp" "$_f" 2>/dev/null || { rm -f "$_f.jsc-tmp"; return 1; }
}
# --- 備份:先備份才准移除 ---
BACKUP_DIR=""
BACKUP_STAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_LIST="" # 每行「{備份檔}<TAB>{原檔}」,還原時反向複製回去
# 備份目錄延後建立:沒有檔案要動時不留空目錄。
# 只設全域變數、不印路徑:呼叫端若寫成 $(backup_dir) 就變成子 shell,設好的 BACKUP_DIR
# 與 BACKUP_LIST 傳不回本 shell,接著的備份與還原全部失準。
ensure_backup_dir() {
[ -z "$BACKUP_DIR" ] || return 0
_d="$JSC_HOME/backup/hooks/$cli/$BACKUP_STAMP"
mkdir -p "$_d" 2>/dev/null || return 1
BACKUP_LIST=$(mktemp) || return 1
BACKUP_DIR="$_d"
return 0
}
# 原樣複製一份到備份目錄,保留原檔名;同名就加 -1、-2 後綴(不同目錄可能有同名檔)。
# 複製失敗回傳 1,呼叫端必須就此停手:沒有備份就移除,等於把使用者的設定弄不見。
backup_file() { # $1=檔案
_src="$1"
[ -f "$_src" ] || return 0
ensure_backup_dir || return 1
_base=$(basename "$_src")
_dst="$BACKUP_DIR/$_base"; _n=0
while [ -e "$_dst" ]; do
_n=$((_n + 1)); _dst="$BACKUP_DIR/$_base-$_n"
done
cp "$_src" "$_dst" 2>/dev/null || return 1
[ -f "$_dst" ] || return 1
printf '%s\t%s\n' "$_dst" "$_src" >> "$BACKUP_LIST" || return 1
return 0
}
# 還原這次所有備份(驗證沒過時用)。已刪除的檔案會被複製回來。
restore_backups() {
[ -n "$BACKUP_LIST" ] && [ -f "$BACKUP_LIST" ] || return 0
while IFS="$(printf '\t')" read -r _b _o; do
[ -n "$_b" ] && [ -n "$_o" ] || continue
mkdir -p "$(dirname "$_o")" 2>/dev/null || true
cp "$_b" "$_o" 2>/dev/null || true
done < "$BACKUP_LIST"
return 0
}
skip() { # $1=reason
printf 'status=skipped reason=%s\n' "$1"
echo "[jsc] 略過:$1"
exit 3
}
fail() { # $1=reason
printf 'status=failed reason=%s\n' "$1"
echo "[jsc] 接線沒生效:$1" >&2
echo "[jsc] 請先以 tools/report-error.sh 回報這次失敗,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2
exit 4
}
# purge 專用的失敗出口:先把備份還原回去,再回報。移除做一半的環境比沒動過更難修。
pfail() { # $1=reason
restore_backups
printf 'status=failed reason=%s\n' "$1"
echo "[jsc] 移除沒完成,已從備份還原:$1" >&2
[ -n "$BACKUP_DIR" ] && echo "[jsc] 備份目錄:$BACKUP_DIR" >&2
echo "[jsc] 請先以 tools/report-error.sh 回報這次失敗,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2
exit 4
}
purged() { # $1=reason
printf 'status=purged reason=%s\n' "$1"
if [ -n "$BACKUP_DIR" ]; then
echo "[jsc] $cli:已移除全部 hook,移除前的原檔備份在 $BACKUP_DIR。"
else
echo "[jsc] $cli:沒有找到任何 hook 設定,已是乾淨狀態,未建立備份目錄。"
fi
}
if [ "$action" = purge ]; then
case "$cli" in
claude)
bin=$(cli_bin claude)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 claude 執行檔"
# claude 的 hook 全部宣告在使用者層設定檔的 hooks 鍵裡,清掉那個鍵就等於清掉所有 hook。
cdir="${JSC_CLAUDE_SETTINGS_DIR:-$HOME/.claude}"
done_files=""
for f in "$cdir/settings.json" "$cdir/settings.local.json"; do
[ -f "$f" ] || continue
# 先問語法:讀不懂的設定檔不能刪鍵,也不能當成「沒有 hooks 鍵」帶過——
# 那會回報 purged 卻留著整套 hook,比直接說失敗更難查。
json_pairs_ok "$f" || pfail "$f 不是成對的 JSON,讀不懂就不動它,請先修好這個檔案"
json_top_key "$f" hooks || continue
backup_file "$f" || pfail "無法備份 $f,沒有備份就不移除"
json_del_hooks "$f" || pfail "無法從 $f 刪除 hooks 鍵"
json_pairs_ok "$f" || pfail "$f 刪除 hooks 鍵後 JSON 括號不成對"
! json_top_key "$f" hooks || pfail "$f 刪除後最上層仍有 hooks 鍵"
done_files="$done_files $f"
done
if [ -n "$done_files" ]; then
purged "已從 claude 使用者層設定檔刪除 hooks 鍵,含非 jsc 的第三方項目"
echo "[jsc] claude:已處理的設定檔:$done_files"
else
purged "claude 使用者層設定檔沒有 hooks 鍵,沒有 hook 要移除"
fi
echo "[jsc] claude:其他 plugin 自帶的 hooks.json 不在使用者設定檔裡,purge 動不到;要靠移除該 plugin 才能清掉。"
echo "[jsc] claude:jsc 自己的 hooks/hooks.json 同樣隨 plugin 提供,移除 jsc-hooks plugin 才會消失。"
exit 0 ;;
codex)
bin=$(cli_bin codex)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 codex 執行檔"
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
config="$CODEX_HOME/config.toml"
agents="$CODEX_HOME/AGENTS.md"
if [ -f "$config" ]; then
if has_block "$config" "# jsc-hooks" || toml_root_key "$config" notify; then
backup_file "$config" || pfail "無法備份 $config,沒有備份就不移除"
remove_block "$config" "# jsc-hooks" "# /jsc-hooks" \
|| pfail "無法從 $config 移除 jsc-hooks 標記段落"
remove_toml_root_key "$config" notify || pfail "無法從 $config 移除根層 notify"
has_block "$config" "# jsc-hooks" && pfail "$config 移除後仍讀得到 jsc-hooks 標記段落"
toml_root_key "$config" notify && pfail "$config 移除後仍有根層 notify"
fi
fi
# rc 檔清所有 `# jsc-hooks*` 段落:別名段落沒有分 CLI 的必要,一次清乾淨最可靠。
rclist=$(mktemp) || pfail "無法建立暫存檔"
rc_files_existing > "$rclist"
while IFS= read -r rc; do
[ -n "$rc" ] || continue
grep -q '^# jsc-hooks' "$rc" 2>/dev/null || continue
backup_file "$rc" || pfail "無法備份 $rc,沒有備份就不移除"
remove_rc_blocks "$rc" || pfail "無法從 $rc 移除 jsc-hooks 標記段落"
grep -q '^# jsc-hooks' "$rc" 2>/dev/null && pfail "$rc 移除後仍有 jsc-hooks 標記段落"
done < "$rclist"
rm -f "$rclist"
if [ -f "$agents" ] && has_block "$agents" "<!-- jsc-hooks -->"; then
backup_file "$agents" || pfail "無法備份 $agents,沒有備份就不移除"
remove_block "$agents" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" \
|| pfail "無法從 $agents 移除 jsc-hooks 標記段落"
has_block "$agents" "<!-- jsc-hooks -->" && pfail "$agents 移除後仍讀得到 jsc-hooks 標記段落"
fi
purged "已移除 config.toml 的標記段落與根層 notify、rc 檔的 jsc-hooks 段落、AGENTS.md 的規則段落"
echo "[jsc] codex:別名要開新的 shell 或重新 source rc 檔才真的失效。"
exit 0 ;;
copilot)
bin=$(cli_bin copilot)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 copilot 執行檔"
instr="${JSC_COPILOT_INSTRUCTIONS:-$HOME/.config/copilot/copilot-instructions.md}"
rclist=$(mktemp) || pfail "無法建立暫存檔"
rc_files_existing > "$rclist"
while IFS= read -r rc; do
[ -n "$rc" ] || continue
has_block "$rc" "# jsc-hooks:copilot" || continue
backup_file "$rc" || pfail "無法備份 $rc,沒有備份就不移除"
remove_block "$rc" "# jsc-hooks:copilot" "# /jsc-hooks:copilot" \
|| pfail "無法從 $rc 移除 jsc-hooks:copilot 段落"
has_block "$rc" "# jsc-hooks:copilot" && pfail "$rc 移除後仍有 jsc-hooks:copilot 段落"
done < "$rclist"
rm -f "$rclist"
if [ -f "$instr" ] && has_block "$instr" "<!-- jsc-hooks -->"; then
backup_file "$instr" || pfail "無法備份 $instr,沒有備份就不移除"
remove_block "$instr" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" \
|| pfail "無法從 $instr 移除 jsc-hooks 標記段落"
has_block "$instr" "<!-- jsc-hooks -->" && pfail "$instr 移除後仍讀得到 jsc-hooks 標記段落"
fi
purged "已移除 rc 檔的 jsc-hooks:copilot 段落與指引檔的規則段落"
echo "[jsc] copilot:別名要開新的 shell 或重新 source rc 檔才真的失效。"
exit 0 ;;
antigravity)
bin=$(cli_bin antigravity)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 antigravity(agy)執行檔"
rules="${JSC_ANTIGRAVITY_RULES:-$HOME/.antigravity/AGENTS.md}"
rclist=$(mktemp) || pfail "無法建立暫存檔"
rc_files_existing > "$rclist"
while IFS= read -r rc; do
[ -n "$rc" ] || continue
has_block "$rc" "# jsc-hooks:antigravity" || continue
backup_file "$rc" || pfail "無法備份 $rc,沒有備份就不移除"
remove_block "$rc" "# jsc-hooks:antigravity" "# /jsc-hooks:antigravity" \
|| pfail "無法從 $rc 移除 jsc-hooks:antigravity 段落"
has_block "$rc" "# jsc-hooks:antigravity" && pfail "$rc 移除後仍有 jsc-hooks:antigravity 段落"
done < "$rclist"
rm -f "$rclist"
if [ -f "$rules" ] && has_block "$rules" "<!-- jsc-hooks -->"; then
backup_file "$rules" || pfail "無法備份 $rules,沒有備份就不移除"
remove_block "$rules" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" \
|| pfail "無法從 $rules 移除 jsc-hooks 標記段落"
has_block "$rules" "<!-- jsc-hooks -->" && pfail "$rules 移除後仍讀得到 jsc-hooks 標記段落"
fi
purged "已移除 rc 檔的 jsc-hooks:antigravity 段落與全域規則檔的規則段落"
echo "[jsc] antigravity:別名要開新的 shell 或重新 source rc 檔才真的失效。"
exit 0 ;;
kiro)
command -v "$(cli_bin kiro)" >/dev/null 2>&1 || skip "未偵測到 kiro-cli 執行檔"
hookdir="./.kiro/hooks"
if [ -d "$hookdir" ]; then
for f in "$hookdir"/*; do
[ -f "$f" ] || continue
backup_file "$f" || pfail "無法備份 $f,沒有備份就不移除"
rm -f "$f" 2>/dev/null || pfail "無法刪除 $f"
done
left=$(find "$hookdir" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')
[ "$left" = 0 ] || pfail "$hookdir 底下還有 $left 個 hook 檔沒刪掉"
fi
purged "已刪除工作區 .kiro/hooks/ 底下所有 hook 檔,含非 jsc 的第三方項目"
echo "[jsc] kiro:hook 檔綁在工作區,這次只清得到目前目錄的 ./.kiro/hooks/,其他工作區要各自跑一次。"
exit 0 ;;
esac
fi
if [ "$action" = smoke ]; then
smoke_out=$(mktemp) || { printf 'status=failed reason=%s\n' "無法建立暫存檔"; exit 4; }
smoke_fails=0
# 跑一支 hook 並判定結果。$1=腳本檔名 $2=子命令(可省略)
# $2 不加引號展開:子命令是固定字面字,空字串時要展成「沒有參數」而不是空參數。
smoke_one() {
_h="$1"; _s="${2:-}"
# 技能名一律清空:冒煙要驗的是「沒有技能情境時腳本跑得完」。留著繼承來的 JSC_SKILL,
# sdlc-gate.sh wp-check skill 會拿它當真實呼叫判定,有未結清 PR 時就誤報成執行期錯誤。
# JSC_CHANGED_FILE 同理清空:留著繼承來的檔名,comment-scope.sh 與 lang-guard.sh 會真的
# 去掃那個檔,掃到違規就 exit 2,冒煙測試變成看環境臉色,測不出腳本本身跑不跑得完。
_out=$(printf '{}' | JSC_CLI="$cli" JSC_SKILL="" SKILL="" JSC_CHANGED_FILE="" \
sh "$HOOKS/$_h" $_s 2>&1); _rc=$?
if [ "$_rc" -eq 0 ]; then
printf '[jsc] %s%s:exit 0,正常。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out"
elif [ "$_h" = sdlc-gate.sh ] && [ "$_s" = check ] && [ "$_rc" -eq 2 ]; then
# 唯一放行的非零退出:sdlc-gate.sh check 的 exit 2 是刻意設計的階段鎖阻擋
# (見 hooks/lib.sh 開頭)——鎖存在且模型不符時就該擋下該輪提示。那是 hook 正常
# 工作,不是執行期錯誤;把它算成錯誤會讓每個正在上鎖的工作階段都誤報一次失敗。
printf '[jsc] %s check:exit 2,SDLC 階段鎖擋下該輪提示,屬設計行為,不算錯誤。\n' "$_h" >> "$smoke_out"
elif [ "$_h" = comment-scope.sh ] && [ "$_rc" -eq 2 ]; then
# 同一類放行:comment-scope.sh 掃描模式的 exit 2 是「掃到違規註解」的設計行為。
# 無參數模式冒煙時取不到檔名,正常會走 exit 0;sweep 則看工作區乾不乾淨——工作區剛好
# 有違規註解就回 2。那是 hook 正常工作,不是 hook 壞掉,不能因此判定接線失敗。
printf '[jsc] %s%s:exit 2,掃到違規註解並發出警告,屬設計行為,不算錯誤。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out"
elif [ "$_h" = lang-guard.sh ] && [ "$_rc" -eq 2 ]; then
# 沿用同一條例外:lang-guard.sh 掃描模式的 exit 2 是「掃到簡體字、亂碼或編碼問題」的
# 設計行為。sweep 一樣看工作區乾不乾淨,髒工作區本來就會回 2,不是 hook 壞掉。
printf '[jsc] %s%s:exit 2,掃到簡體字或亂碼並發出警告,屬設計行為,不算錯誤。\n' "$_h" "${_s:+ $_s}" >> "$smoke_out"
else
smoke_fails=$((smoke_fails + 1))
printf '[jsc] %s%s:exit %s,執行期出錯:%s\n' "$_h" "${_s:+ $_s}" "$_rc" \
"$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
fi
}
smoke_one session-timer.sh mark
smoke_one sdlc-gate.sh check
smoke_one version-guard.sh
smoke_one restart-gate.sh
smoke_one skill-usage.sh
smoke_one ste100-guard.sh
# sdlc-gate.sh 有兩個 hook 模式,接在不同事件上,兩個都要驗:wp-check prompt 一律 exit 0,
# wp-check skill 在取不到技能名時放行(上面已清空技能名),所以兩者都不需要白名單例外。
smoke_one sdlc-gate.sh "wp-check prompt"
smoke_one sdlc-gate.sh "wp-check skill"
# 工作包歸屬判定(wp-check 的第二段):上面兩支清空技能名,只走得到「沒有未結清 PR」與
# 「取不到技能名」兩條捷徑,歸屬比對整段都沒跑到。這裡自備一份暫時的 $JSC_HOME 狀態檔,
# 把每條判定路徑各跑一次,驗的是判定結果本身,不只是腳本跑得完。用暫時目錄是為了不動到
# 使用者真正的 $JSC_HOME/wp/——冒煙測試不該在別人的鎖檔上留下痕跡。
# 一律 </dev/null:wp-check 會讀標準輸入,管線沒人關閉時整支卡死。
smoke_wp_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=JSC_WP_GATE 值(可省略)
_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=$?
if [ "$_rc" -eq "$3" ]; then
printf '[jsc] sdlc-gate.sh wp-check skill(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out"
else
smoke_fails=$((smoke_fails + 1))
printf '[jsc] sdlc-gate.sh wp-check skill(%s):exit %s,預期 %s,歸屬判定壞了:%s\n' \
"$1" "$_rc" "$3" "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
fi
}
if wp_home=$(mktemp -d 2>/dev/null) && mkdir -p "$wp_home/wp" 2>/dev/null; then
smoke_wp_case "狀態檔不存在" plan 0
printf 'repo=jsc/smoke\nwp=WP-03\npr=12\n' > "$wp_home/wp/jsc-smoke.claim"
printf 'repo=jsc/smoke\nindex=12\nwp=WP-03\n' > "$wp_home/wp/jsc-smoke-12.pr"
smoke_wp_case "PR 屬於領取中的工作包,技能 implement" implement 0
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 屬於別的工作包,技能 implement" implement 0
smoke_wp_case "逃生門 JSC_WP_GATE=off" plan 0 off
rm -rf "$wp_home"
else
smoke_fails=$((smoke_fails + 1))
printf '[jsc] sdlc-gate.sh wp-check skill:建不出暫存目錄,工作包歸屬判定沒驗到。\n' >> "$smoke_out"
fi
# 部署後重啟閘門(restart-gate.sh):上面那支只走得到「狀態檔不存在」與「取不到技能名」,
# 擋人那一條完全沒跑到。這裡同樣自備一份暫時的 $JSC_HOME,把每條判定路徑各跑一次並比對
# 結束碼,另外驗三件狀態檔設計本身的事:別支 CLI 那一份不影響這一支、清除只刪自己那一份、
# 舊格式的單一狀態檔照樣擋得下來。用暫時目錄是為了不動到使用者真正的
# $JSC_HOME/restart-required.d/——冒煙測試不該把別人的閘門拆掉。
# 一律 </dev/null:hook 模式會讀標準輸入,管線沒人關閉時整支卡死。
smoke_rs_case() { # $1=情境 $2=技能名 $3=預期結束碼 $4=JSC_RESTART_GATE 值(可省略)
_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=$?
if [ "$_rc" -eq "$3" ]; then
printf '[jsc] restart-gate.sh(%s):exit %s,與預期相同。\n' "$1" "$_rc" >> "$smoke_out"
else
smoke_fails=$((smoke_fails + 1))
printf '[jsc] restart-gate.sh(%s):exit %s,預期 %s,重啟閘門判定壞了:%s\n' \
"$1" "$_rc" "$3" "$(printf '%s' "$_out" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
fi
}
# 狀態檔在不在也要比:一支 CLI 一份的重點就在「該留的留、該刪的刪」,只看結束碼看不出來。
smoke_rs_file() { # $1=情境 $2=狀態檔 $3=exist 或 absent
if { [ "$3" = exist ] && [ -f "$2" ]; } || { [ "$3" = absent ] && [ ! -f "$2" ]; }; then
printf '[jsc] restart-gate.sh(%s):狀態檔 %s,與預期相同。\n' "$1" "$3" >> "$smoke_out"
else
smoke_fails=$((smoke_fails + 1))
printf '[jsc] restart-gate.sh(%s):狀態檔預期 %s,實際不是,一支 CLI 一份的範圍壞了:%s\n' \
"$1" "$3" "$2" >> "$smoke_out"
fi
}
if rs_home=$(mktemp -d 2>/dev/null); then
rs_dir="$rs_home/restart-required.d"
# 別支 CLI 的代號。取一個不在五支之列的固定值,才不會跟這一輪的 $cli 撞在一起。
rs_other=smoke-other
smoke_rs_case "當前 CLI 沒有狀態檔" jsc-sdlc:implement 0
JSC_HOME="$rs_home" JSC_CLI="$rs_other" \
sh "$HOOKS/restart-gate.sh" require update hooks </dev/null 2>/dev/null
smoke_rs_case "只有別支 CLI 有狀態檔" jsc-sdlc:implement 0
JSC_HOME="$rs_home" JSC_CLI="$cli" \
sh "$HOOKS/restart-gate.sh" require update hooks cli </dev/null 2>/dev/null
smoke_rs_file "require 寫出當前 CLI 那一份" "$rs_dir/$cli" exist
smoke_rs_case "當前 CLI 那份存在,技能 jsc-sdlc:implement" jsc-sdlc:implement 2
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-cli:deploy" jsc-cli:deploy 0
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-gitea:wiki" jsc-gitea:wiki 0
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-log:worklog" jsc-log:worklog 0
smoke_rs_case "當前 CLI 那份存在,豁免技能 jsc-meta:skill-check" jsc-meta:skill-check 0
smoke_rs_case "逃生門 JSC_RESTART_GATE=off" jsc-sdlc:implement 0 off
smoke_rs_case "取不到技能名" "" 0
# 清除機制:session-timer.sh 判定為新工作階段時會呼叫 restart-gate.sh clear。
# 這裡走的就是那條路徑(暫時 $JSC_HOME 底下沒有起始檔,等同行程新起的第一次)。
JSC_HOME="$rs_home" JSC_CLI="$cli" JSC_SESSION_ID=smoke-restart \
sh "$HOOKS/session-timer.sh" start </dev/null 2>/dev/null
smoke_rs_file "新工作階段開始後清掉自己那一份" "$rs_dir/$cli" absent
smoke_rs_file "清除不動別支 CLI 那一份" "$rs_dir/$rs_other" exist
smoke_rs_case "清除後放行" jsc-sdlc:implement 0
# 舊格式的單一狀態檔(過渡相容):沒有 per-CLI 資訊,所以一律擋,clear 一併刪掉。
printf 'at=%s\nmode=update\ndomains=hooks\ncli=%s\n' "$(now_iso)" "$rs_other" \
> "$rs_home/restart-required" 2>/dev/null
smoke_rs_case "舊格式單一狀態檔存在" jsc-sdlc:implement 2
JSC_HOME="$rs_home" JSC_CLI="$cli" JSC_SESSION_ID=smoke-restart-legacy \
sh "$HOOKS/session-timer.sh" restart </dev/null 2>/dev/null
smoke_rs_file "清除一併刪掉舊格式狀態檔" "$rs_home/restart-required" absent
smoke_rs_case "舊格式狀態檔清除後放行" jsc-sdlc:implement 0
rm -rf "$rs_home"
else
smoke_fails=$((smoke_fails + 1))
printf '[jsc] restart-gate.sh:建不出暫存目錄,部署後重啟閘門判定沒驗到。\n' >> "$smoke_out"
fi
# comment-scope.sh 有三個接在不同事件的模式,三個都要驗:prompt 一律 exit 0,
# 無參數模式在取不到檔名時安靜 exit 0(上面已清空 JSC_CHANGED_FILE,stdin 也只有 {}),
# sweep 掃目前工作目錄所在的 git 工作區——乾淨或非 git 目錄回 0,有違規註解回 2,
# 後者由上面的白名單放行(見 smoke_one)。
smoke_one comment-scope.sh prompt
smoke_one comment-scope.sh
smoke_one comment-scope.sh sweep
# lang-guard.sh 同樣有三個接在不同事件的模式,三個都要驗,判定理由與 comment-scope.sh 相同:
# prompt 一律 exit 0,無參數模式取不到檔名時安靜 exit 0,sweep 在髒工作區回 2 由白名單放行。
smoke_one lang-guard.sh prompt
smoke_one lang-guard.sh
smoke_one lang-guard.sh sweep
if [ "$smoke_fails" -eq 0 ]; then
printf 'status=ok reason=%s\n' "八支 hook 的每個接線模式都跑得完,工作包歸屬判定與部署後重啟閘門的每條路徑也各走過一次,沒有執行期錯誤"
cat "$smoke_out"; rm -f "$smoke_out"; exit 0
fi
printf 'status=failed reason=%s\n' "$smoke_fails 支 hook 有執行期錯誤"
cat "$smoke_out"
rm -f "$smoke_out"
echo "[jsc] 請先以 tools/report-error.sh 回報,再交給 /jsc-hooks:repair 自動修正並開 develop PR。" >&2
exit 4
fi
if [ "$action" = status ]; then
# 唯讀盤點:只讀設定檔判斷標記段落在不在,不寫檔,也不執行任何 hook。
# 檔案位置與標記字串一律沿用底下接線區塊的同一份值,兩邊必須一起改。
command -v "$(cli_bin "$cli")" >/dev/null 2>&1 || skip "未偵測到 $(cli_bin "$cli") 執行檔"
st_items=$(mktemp) || { printf 'status=failed reason=%s\n' "無法建立暫存檔"; exit 4; }
st_missing=0
st_degrade=""
# 記一個檢查點。$1=項目名 $2=路徑 $3=present|missing
st_item() {
printf 'item\t%s\t%s\t%s\n' "$1" "$2" "$3" >> "$st_items"
[ "$3" = present ] || st_missing=$((st_missing + 1))
}
# 檔案存在且帶有該標記才算接上。$1=項目名 $2=檔案 $3=開頭標記行
st_block() {
if [ -f "$2" ] && has_block "$2" "$3"; then st_item "$1" "$2" present
else st_item "$1" "$2" missing; fi
}
# 註解範圍規則寫進規則檔了嗎。與 STE100 共用同一個標記段落,所以要單獨比對內容:
# 標記在、內容卻是舊版只有 STE100 的那一份時,這一項才看得出來缺了。$1=項目名 $2=檔案
st_comment_scope() {
if [ -f "$2" ] && has_comment_scope "$2"; then st_item "$1" "$2" present
else st_item "$1" "$2" missing; fi
}
# 繁中與編碼規則寫進規則檔了嗎。同樣與 STE100 共用標記段落,所以要單獨比對內容:
# 標記在、內容卻是舊版沒有這一段時,這一項才看得出來缺了。$1=項目名 $2=檔案
st_lang_guard() {
if [ -f "$2" ] && has_lang_guard "$2"; then st_item "$1" "$2" present
else st_item "$1" "$2" missing; fi
}
# 別名段落只要任何一個既有 rc 檔帶有標記就算接上。$1=項目名 $2=標記名
st_rc_alias() {
for _rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do
[ -f "$_rc" ] || continue
if has_block "$_rc" "# $2"; then st_item "$1" "$_rc" present; return 0; fi
done
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)
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 "$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"
st_block notify "$config" "# jsc-hooks"
# 標記在、鍵卻被歸進某張表時 codex 讀不到 notify,等同沒接,所以位置要單獨算一項
if [ -f "$config" ] && toml_root_key "$config" notify; then
st_item notify-root "$config" present
else
st_item notify-root "$config" missing
fi
# notify 接上了,不代表 sweep 也串進那一行:舊版接線只有計時,掃描是後來才加的
if [ -f "$config" ] && grep -qF 'comment-scope.sh' "$config" 2>/dev/null &&
grep -qF 'sweep' "$config" 2>/dev/null; then
st_item notify-sweep "$config" present
else
st_item notify-sweep "$config" missing
fi
# 兩支 sweep 各算一項:只驗其中一支會讓「舊版只接了註解範圍」看起來像接線完整
if [ -f "$config" ] && grep -qF 'lang-guard.sh' "$config" 2>/dev/null &&
grep -qF 'sweep' "$config" 2>/dev/null; then
st_item notify-lang-sweep "$config" present
else
st_item notify-lang-sweep "$config" missing
fi
st_rc_alias alias jsc-hooks:codex
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"
_codex_plugin_hooks=$(codex_plugin_hooks_json || true)
if codex_plugin_hooks_safe; then
st_item plugin-user-prompt-root "${_codex_plugin_hooks:-${CODEX_HOME:-$HOME/.codex}/plugins/cache/jsc/jsc-hooks}" present
else
st_item plugin-user-prompt-root "$_codex_plugin_hooks" missing
fi
st_degrade="STE100 降級為 prompt 檔,SDLC 模型鎖降級為技能步驟檢查,無 pre-tool hook 可接版本前置檢查與部署後重啟閘門,註解範圍與繁中編碼改為每輪結束掃整個工作區,不是逐檔即時" ;;
copilot)
st_rc_alias alias jsc-hooks:copilot
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)
st_rc_alias alias jsc-hooks:antigravity
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)
# kiro 的 hook 檔綁在工作區,這裡看的一律是目前工作目錄底下那一份
for _f in ./.kiro/hooks/jsc-hooks-session-start.json ./.kiro/hooks/jsc-hooks.json; do
if [ -f "$_f" ] && json_top_key "$_f" run; then st_item "$(basename "$_f" .json)" "$_f" present
else st_item "$(basename "$_f" .json)" "$_f" missing; fi
done
# userPromptSubmit 那一筆的 run 串了好幾支腳本,只驗 JSON 讀得懂會漏掉少接的那一支。
# comment-scope.sh 的 prompt 與 sweep 是兩件事,各算一項,才看得出舊版接線少了哪一個。
if [ -f ./.kiro/hooks/jsc-hooks.json ] &&
grep -qF 'comment-scope.sh\" prompt' ./.kiro/hooks/jsc-hooks.json 2>/dev/null
then st_item comment-scope ./.kiro/hooks/jsc-hooks.json present
else st_item comment-scope ./.kiro/hooks/jsc-hooks.json missing; fi
if [ -f ./.kiro/hooks/jsc-hooks.json ] &&
grep -qF 'comment-scope.sh\" sweep' ./.kiro/hooks/jsc-hooks.json 2>/dev/null
then st_item comment-scope-sweep ./.kiro/hooks/jsc-hooks.json present
else st_item comment-scope-sweep ./.kiro/hooks/jsc-hooks.json missing; fi
# lang-guard.sh 的兩個模式同理各算一項
if [ -f ./.kiro/hooks/jsc-hooks.json ] &&
grep -qF 'lang-guard.sh\" prompt' ./.kiro/hooks/jsc-hooks.json 2>/dev/null
then st_item lang-guard ./.kiro/hooks/jsc-hooks.json present
else st_item lang-guard ./.kiro/hooks/jsc-hooks.json missing; fi
if [ -f ./.kiro/hooks/jsc-hooks.json ] &&
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
if [ "$st_missing" -gt 0 ]; then
printf 'status=unwired reason=%s\n' "$st_missing 個接線項目缺漏,執行 /jsc-hooks:hooks-install 重新接線"
cat "$st_items"; rm -f "$st_items"; exit 5
fi
if [ -n "$st_degrade" ]; then
printf 'status=degraded reason=%s\n' "$st_degrade"
cat "$st_items"; rm -f "$st_items"; exit 1
fi
printf 'status=wired reason=%s\n' "hooks.json 自動接線全部八支 hook"
cat "$st_items"; rm -f "$st_items"; exit 0
fi
ensure_stable_root
verify_wire_root
case "$cli" in
claude)
bin=$(cli_bin claude)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 claude 執行檔"
[ -f "$HOOKS/hooks.json" ] || fail "找不到 $HOOKS/hooks.json,claude 接不到任何 hook"
grep -qF 'comment-scope.sh' "$HOOKS/hooks.json" 2>/dev/null \
|| fail "$HOOKS/hooks.json 沒有接上 comment-scope.sh,註解範圍檢查不會生效"
grep -qF 'lang-guard.sh' "$HOOKS/hooks.json" 2>/dev/null \
|| fail "$HOOKS/hooks.json 沒有接上 lang-guard.sh,繁中與編碼檢查不會生效"
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 掃整個工作區,時機晚一輪或晚到工作階段結束。"
exit 0 ;;
codex)
bin=$(cli_bin codex)
command -v "$bin" >/dev/null 2>&1 || skip "未偵測到 codex 執行檔"
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
config="$CODEX_HOME/config.toml"
agents="$CODEX_HOME/AGENTS.md"
# codex 的 notify 只在每一輪結束時觸發,沒有工作階段開始事件,所以計時分兩段接:
# 1. shell 別名走 jsc-wrap.sh:啟動當下就 session-timer start,並給這次工作階段
# 一個 JSC_SESSION_ID,codex 內觸發的 notify 會沿用同一個 id。
# 2. notify 每輪先補 start 再 mark。start 已有紀錄就不動,所以沒走別名啟動時
# 仍拿得到起始時間(從第一輪算起)。少了這一段,worklog 只會拿到 0 秒。
# notify 最後再掛 comment-scope.sh sweep:codex 沒有 post-tool hook,拿不到「剛剛寫了
# 哪個檔」,只能改掃整個 git 工作區的 diff。每輪結束掃一次,時機比 claude 晚,那一輪
# 寫過的檔一個都不會漏。
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 \"$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 標記段落"
toml_root_key "$config" notify \
|| fail "$config 的 notify 沒有落在根層(被歸進某張表,codex 讀不到,hook 會靜靜失效)"
grep -qF "$scope sweep" "$config" 2>/dev/null \
|| fail "$config 的 notify 沒有接到 comment-scope.sh sweep,codex 每輪結束不會掃註解範圍"
grep -qF "$lang sweep" "$config" 2>/dev/null \
|| fail "$config 的 notify 沒有接到 lang-guard.sh sweep,codex 每輪結束不會掃簡體字與亂碼"
write_alias_rc "jsc-hooks:codex" "$alias_line" || fail "無法把 $bin 別名寫進 shell rc 檔"
replace_block "$agents" "<!-- jsc-hooks -->" "<!-- /jsc-hooks -->" "$(rules_text)" \
|| fail "無法寫入 $agents"
has_block "$agents" "<!-- jsc-hooks -->" || fail "$agents 寫入後讀不到 jsc-hooks 標記段落"
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。"
echo "[jsc] codex:已在 $agents 寫入 STE100、註解範圍與繁中編碼規則段落(prompt 降級)。"
echo "[jsc] codex:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] codex:版本前置檢查接不上(codex 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
echo "[jsc] codex:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。"
echo "[jsc] codex:註解範圍與繁中編碼除了規則提示,每輪結束會由 notify 各掃一次整個 git 工作區(codex 沒有 post-tool hook,接不到逐檔即時掃描),回饋比 claude 晚一輪。"
exit 1 ;;
copilot)
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 \"$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"
has_block "$instr" "<!-- jsc-hooks -->" || fail "$instr 寫入後讀不到 jsc-hooks 標記段落"
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 檔才生效。"
echo "[jsc] copilot:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] copilot:版本前置檢查接不上(copilot 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
echo "[jsc] copilot:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。"
echo "[jsc] copilot:註解範圍與繁中編碼除了規則提示,工作階段結束時由 jsc-wrap.sh 收尾各掃一次整個 git 工作區(copilot 連逐輪事件都沒有),回饋要等到離開 CLI 才看得到。"
exit 1 ;;
antigravity)
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 \"$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"
has_block "$rules" "<!-- jsc-hooks -->" || fail "$rules 寫入後讀不到 jsc-hooks 標記段落"
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 檔才生效。"
echo "[jsc] antigravity:SDLC 模型鎖降級為技能步驟檢查,鎖檔仍由 sdlc-gate.sh lock 寫入。"
echo "[jsc] antigravity:版本前置檢查接不上(antigravity 沒有 pre-tool hook),改由 /jsc-cli:deploy 定期更新。"
echo "[jsc] antigravity:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。"
echo "[jsc] antigravity:註解範圍與繁中編碼除了規則提示,工作階段結束時由 jsc-wrap.sh 收尾各掃一次整個 git 工作區(antigravity 連逐輪事件都沒有),回饋要等到離開 CLI 才看得到。"
exit 1 ;;
kiro)
command -v "$(cli_bin kiro)" >/dev/null 2>&1 || skip "未偵測到 kiro-cli 執行檔"
hookdir="./.kiro/hooks"
hookfile="$hookdir/jsc-hooks.json"
startfile="$hookdir/jsc-hooks-session-start.json"
mkdir -p "$hookdir" 2>/dev/null || fail "無法建立 $hookdir"
# 計時要分兩個檔:kiro 的一個 hook 檔只有一組 run,所有事件共用。
# sessionStart 單獨一檔跑 restart,才算得出這一次工作階段的花費時間;
# kiro 給不到 session id,紀錄共用 default,不覆寫起始時間就會把上一階段算進來。
cat > "$startfile" 2>/dev/null <<EOF || fail "無法寫入 $startfile"
{
"name": "jsc-hooks-session-start",
"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 \\"$WIRE_HOOKS/session-timer.sh\\" restart </dev/null"
}
EOF
# userPromptSubmit 那一輪的 run 最後再掛 comment-scope.sh sweep 與 lang-guard.sh sweep:
# kiro 沒有 post-tool hook,拿不到「剛剛寫了哪個檔」,只能改掃整個 git 工作區的 diff。
# 掃到的是上一輪寫的檔——提示送出時,上一輪的寫入早就落地了,時機合理。
cat > "$hookfile" 2>/dev/null <<EOF || fail "無法寫入 $hookfile"
{
"name": "jsc-hooks",
"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 \\"$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
json_top_key "$f" on || fail "$f 不是成對的 JSON,或 on 不在最上層"
json_top_key "$f" run || fail "$f 不是成對的 JSON,或 run 不在最上層"
grep -qF '"JSC_CLI": "kiro"' "$f" 2>/dev/null || fail "$f 缺少 JSC_CLI=kiro"
grep -qF 'session-timer.sh' "$f" 2>/dev/null || fail "$f 的 run 沒有接到 session-timer.sh"
done
grep -qF '"sessionStart"' "$startfile" 2>/dev/null || fail "$startfile 沒有接在 sessionStart"
grep -qF '"userPromptSubmit"' "$hookfile" 2>/dev/null || fail "$hookfile 沒有接在 userPromptSubmit"
grep -qF 'comment-scope.sh' "$hookfile" 2>/dev/null || fail "$hookfile 的 run 沒有接到 comment-scope.sh"
# 兩個模式接的是兩件事,只驗腳本名會漏掉少接的那一個,所以各驗一次
grep -qF 'comment-scope.sh\" prompt' "$hookfile" 2>/dev/null \
|| fail "$hookfile 的 run 沒有接到 comment-scope.sh prompt,每輪不會注入註解範圍規則"
grep -qF 'comment-scope.sh\" sweep' "$hookfile" 2>/dev/null \
|| fail "$hookfile 的 run 沒有接到 comment-scope.sh sweep,kiro 每輪不會掃註解範圍"
grep -qF 'lang-guard.sh' "$hookfile" 2>/dev/null || fail "$hookfile 的 run 沒有接到 lang-guard.sh"
grep -qF 'lang-guard.sh\" prompt' "$hookfile" 2>/dev/null \
|| fail "$hookfile 的 run 沒有接到 lang-guard.sh prompt,每輪不會注入繁中與編碼規則"
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 定期更新。"
echo "[jsc] kiro:部署後重啟閘門也接不上(同樣是沒有 pre-tool hook),一次技能呼叫都擋不下來;狀態檔照樣寫、下次工作階段開始照樣清,重啟要自己動手。"
echo "[jsc] kiro:註解範圍與繁中編碼除了規則提示,每輪提示送出時會各掃一次整個 git 工作區(kiro 沒有 post-tool hook),掃到的是上一輪寫的檔。"
exit 1 ;;
esac