新增第九支 hook,共四種模式。stage 在計畫與分析階段鎖著時擋下寫檔。 review 在稽核類技能執行中擋下寫檔。commit 擋下「一次加入全部變更再提交」的 單一指令,也擋下含簡體字、亂碼或非 UTF-8 編碼的提交訊息。 release 不接 hook,由稽核技能收尾時自己呼叫,清掉認人用的那份紀錄, 讓呼叫端接手修改時不會被剛跑完的稽核擋住。 這三條規則原本只寫在技能內文,靠模型自律。稽核技能會順手改程式碼, 計畫階段會寫出不該寫的檔案,整包提交會把型別與功能分組壓成一次。 規則要真的生效,就得由程式擋。release 模式是必要的:紀錄記的是最近一次 載入的技能,不是還在跑的技能,沒有它,稽核跑完之後呼叫端每一次寫入都被擋, 閘門會把解除自己的路徑一起鎖掉。 接線設定把 stage 與 review 接在寫檔工具,把 commit 接在指令工具。 閘門只讀階段閘門的狀態鎖與技能用量的紀錄,不自己寫狀態檔; 簡繁與編碼判定整段轉呼叫語言守門,不留第二份字表。 接線腳本的接線、唯讀盤點與冒煙各補上這一支,冒煙自備暫時的狀態目錄, 逐條比對每種模式的結束碼。接線腳本另補唯讀模式,體檢類技能全程帶著它跑, 清除與接線一律拒絕並回非零,子命令打錯一個字也改不到環境。 冒煙改成自己數結果行並自我斷言,散文只引用那一行,不再各抄一份數字。 限制據實寫在檔頭:只有 claude 有寫檔前置鉤子,其餘四支 CLI 一條都接不上, 那四支上這三條規則仍只剩技能內文。
1439 lines
83 KiB
Bash
Executable File
1439 lines
83 KiB
Bash
Executable File
#!/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
|
||
#
|
||
# JSC_READONLY=1 時只准 status 與 smoke,purge 與接線一律拒絕並回 exit 6。體檢類技能全程帶著
|
||
# 這個變數跑,「子命令打錯一個字就重新接線或刪檔」的風險就由程式擋掉,不靠呼叫端自我約束。
|
||
#
|
||
# 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 那份存在、豁免技能、逃生門、取不到技能名各跑一次,再驗狀態檔本身在不在——清除只刪自己
|
||
# 那一份、別支那一份留著、舊格式的單一狀態檔照樣擋得下來且清得掉。
|
||
# write-guard.sh 的四種模式同法:階段鎖、稽核技能、提交指令與 release 解除各自的判定路徑,
|
||
# 都用暫時的 $JSC_HOME 跑過一次並比對結束碼。
|
||
#
|
||
# smoke 自己數結果行並自我斷言:`lines<TAB>{數量}` 那一行印的是其後 `[jsc]` 結果行的實際條數,
|
||
# 與腳本內宣告的預期條數逐類比對,不符就回非零。數字寫在腳本裡、由腳本自己印,散文引用那一行
|
||
# 就好,不必在 SKILL.md 或 README 各抄一份——抄了就會在加減判定路徑時漂移。
|
||
#
|
||
# 接線行為(依 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(該接的段落缺了至少一項)
|
||
# 結束碼(唯讀模式): 6=readonly(JSC_READONLY=1 之下拒絕 purge 與接線),status 與 smoke 不受影響
|
||
# 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
|
||
|
||
# 唯讀契約在程式層把關,不靠呼叫端記得只打 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_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
|
||
|
||
# 每一類實際跑過的結果行數。收尾時與下面宣告的預期條數逐類比對,加減判定路徑卻忘了改預期
|
||
# 就會當場失敗,散文與程式之間不會再各記一份數字。
|
||
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=子命令(可省略)
|
||
# $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=$?
|
||
smoke_n_hook=$((smoke_n_hook + 1))
|
||
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"
|
||
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
|
||
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"
|
||
|
||
smoke_model_case() { # $1=情境 $2=來源模式 $3=預期來源片段
|
||
_name="$1"; _mode="$2"; _want="$3"
|
||
_home=$(mktemp -d 2>/dev/null) || {
|
||
smoke_fails=$((smoke_fails + 1))
|
||
printf '[jsc] sdlc-gate.sh model:建不出暫存目錄,模型來源判定沒驗到。\n' >> "$smoke_out"
|
||
return
|
||
}
|
||
mkdir -p "$_home/sessions" "$_home/codex/sessions/2026/08/28" 2>/dev/null
|
||
printf 'stage\tplan\treasoning-max\nmodel\tgpt-5.x\treasoning-max,reasoning-high,coding\nmodel\tgpt-5.x-mini\tcoding,fast,cheap\n' > "$_home/model-tags.tsv"
|
||
_tp="$_home/transcript.jsonl"
|
||
_cf="$_home/codex/sessions/2026/08/28/rollout-2026-08-28T00-00-00-smoke-model.jsonl"
|
||
printf '{"type":"assistant","model":"gpt-5.x"}\n' > "$_tp"
|
||
printf '{"type":"turn_context","payload":{"model":"gpt-5.x"}}\n' > "$_cf"
|
||
case "$_mode" in
|
||
transcript) _json='{"transcript_path":"'"$_tp"'","session_id":"smoke-model"}'; _env="" ;;
|
||
codex) _json='{"session_id":"smoke-model"}'; _env="" ;;
|
||
stdin) _json='{"model":"gpt-5.x","session_id":"smoke-model-stdin"}'; _env="" ;;
|
||
override) _json='{"session_id":"smoke-model-override"}'; _env="JSC_MODEL=gpt-5.x" ;;
|
||
esac
|
||
if [ "$_mode" = override ]; then
|
||
_out=$(printf '%s' "$_json" | JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$cli" JSC_MODEL="gpt-5.x" sh "$HOOKS/sdlc-gate.sh" lock plan 2>&1); _rc=$?
|
||
else
|
||
_out=$(printf '%s' "$_json" | JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$cli" sh "$HOOKS/sdlc-gate.sh" lock plan 2>&1); _rc=$?
|
||
fi
|
||
_rep=$(JSC_HOME="$_home" CODEX_HOME="$_home/codex" JSC_CLI="$cli" \
|
||
sh "$HOOKS/sdlc-gate.sh" report </dev/null 2>/dev/null)
|
||
if [ "$_rc" -eq 0 ] && printf '%s\n%s' "$_out" "$_rep" | grep -qF "$_want"; then
|
||
printf '[jsc] sdlc-gate.sh model(%s):exit 0,來源含 %s,與預期相同。\n' "$_name" "$_want" >> "$smoke_out"
|
||
else
|
||
smoke_fails=$((smoke_fails + 1))
|
||
printf '[jsc] sdlc-gate.sh model(%s):exit %s,來源未命中 %s,輸出:%s\n' \
|
||
"$_name" "$_rc" "$_want" "$(printf '%s %s' "$_out" "$_rep" | tr '\n' ' ' | cut -c1-200)" >> "$smoke_out"
|
||
fi
|
||
rm -rf "$_home"
|
||
}
|
||
smoke_model_case "transcript 實查" transcript "transcript:"
|
||
smoke_model_case "Codex session 記錄" codex "codex-session:"
|
||
smoke_model_case "stdin model 欄位" stdin "hook-stdin"
|
||
smoke_model_case "JSC_MODEL 人工覆寫" override "人工覆寫:JSC_MODEL"
|
||
|
||
# 工作包歸屬判定(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=$?
|
||
smoke_n_wp=$((smoke_n_wp + 1))
|
||
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 屬於別的工作包,技能 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 "逃生門 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=$?
|
||
smoke_n_rs=$((smoke_n_rs + 1))
|
||
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
|
||
smoke_n_rs=$((smoke_n_rs + 1))
|
||
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
|
||
# 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
|
||
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
|
||
fi
|
||
printf 'status=failed reason=%s\n' "$smoke_fails 條判定有執行期錯誤"
|
||
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 [ "$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 與 write-guard.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
|
||
# 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)
|
||
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,部署後重啟閘門不會生效"
|
||
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 自動接線"
|
||
echo "$WIRE_PATH_NOTE"
|
||
echo "[jsc] claude:由 hooks/hooks.json 自動接線全部九支 hook,無需寫入設定。"
|
||
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 掃整個工作區,時機晚一輪或晚到工作階段結束。"
|
||
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:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。"
|
||
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:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。"
|
||
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:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。"
|
||
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:write-guard.sh 的階段寫入、稽核寫入與提交檢查三種模式也接不上(同樣是沒有 pre-tool hook),一次寫入或提交都擋不下來,那三條規則在這裡只剩 SKILL.md 的散文。"
|
||
echo "[jsc] kiro:註解範圍與繁中編碼除了規則提示,每輪提示送出時會各掃一次整個 git 工作區(kiro 沒有 post-tool hook),掃到的是上一輪寫的檔。"
|
||
exit 1 ;;
|
||
esac
|