feat(hooks-install): 安裝改為先清空所有 hook 再接線並驗執行期
This commit is contained in:
Executable
+190
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env sh
|
||||
# scan-hook-errors.sh — 掃 CLI 的原生紀錄,找出 hook 的「執行期」錯誤:接線寫對了、
|
||||
# hook 也真的被觸發了,但跑起來出錯(缺執行檔、路徑錯、權限不足)。
|
||||
# 用法: scan-hook-errors.sh --cli {claude|codex|copilot|antigravity|kiro}
|
||||
#
|
||||
# 覆蓋範圍要據實回報,不得暗示每個 CLI 都掃得到:
|
||||
# claude 有 hook 結果紀錄,掃 ~/.claude/projects/**/*.jsonl 兩種紀錄——
|
||||
# `"type":"hook_non_blocking_error"` 的 attachment
|
||||
# (hookName、hookEvent、exitCode、stderr、command、timestamp)
|
||||
# 與非空的 `"hookErrors":[...]`
|
||||
# codex、copilot、antigravity、kiro 原生紀錄只留工作階段與提示內容,沒有記下 hook 的退出碼與
|
||||
# stderr,一律回報 unavailable,改用 wire-cli.sh smoke {cli}
|
||||
# 主動跑一輪驗執行期
|
||||
#
|
||||
# 去重: 以 $JSC_HOME/errors/scan-state/ 記住每個日誌檔已掃描的位元組數(同 tools/scan-logs.sh),
|
||||
# 重掃只讀新增段落。兩種紀錄各自成筆,不互相配對:同一次失敗在不同 transcript 條目
|
||||
# 裡各留一筆,靠位置猜配對只會把真錯誤併掉。
|
||||
#
|
||||
# 產出: 每筆錯誤附加一行 JSON 到 $JSC_HOME/errors/hooks.jsonl(格式比照 hooks/skill-usage.sh):
|
||||
# {ts,cli,hook,event,exit,detail,jsc}
|
||||
# jsc 欄位:command 或 stderr 命中 ste100-guard.sh、session-timer.sh、skill-usage.sh、
|
||||
# sdlc-gate.sh、version-guard.sh 任一支就是 true,否則 false。分得出來才用得上——
|
||||
# 非 jsc 的 hook 錯誤不是 jsc 該修的,hooks-install 只回報、不轉 jsc-hooks:repair。
|
||||
#
|
||||
# 輸出: 第一行 `status={clean|errors|unavailable} reason=...`(可供程式判讀),
|
||||
# errors 時其後每筆一行人類可讀的繁中摘要(hook 名、退出碼、是否屬 jsc)。
|
||||
# 結束碼: 0=clean 或 unavailable、1=errors、2=用法錯誤
|
||||
set -u
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$HERE/../hooks/lib.sh"
|
||||
|
||||
cli=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--cli) cli="${2:-}"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
case "$cli" in
|
||||
claude|codex|copilot|antigravity|kiro) ;;
|
||||
*)
|
||||
echo "用法:scan-hook-errors.sh --cli {claude|codex|copilot|antigravity|kiro}" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
|
||||
ERRDIR="$JSC_HOME/errors"
|
||||
STATE="$ERRDIR/scan-state"
|
||||
OUT="$ERRDIR/hooks.jsonl"
|
||||
mkdir -p "$STATE" 2>/dev/null || true
|
||||
|
||||
case "$cli" in
|
||||
codex|copilot|antigravity|kiro)
|
||||
printf 'status=unavailable reason=%s\n' "$cli 沒有 hook 結果紀錄,執行期錯誤掃不到"
|
||||
echo "[jsc] $cli:原生紀錄只留工作階段與提示內容,沒有記下 hook 的退出碼與 stderr。"
|
||||
echo "[jsc] $cli:改跑 tools/wire-cli.sh smoke $cli,主動執行五支 hook 驗執行期。"
|
||||
exit 0 ;;
|
||||
esac
|
||||
|
||||
# 印出日誌檔自上次掃描後的新增內容,並更新位移(位移即去重機制)
|
||||
new_content() { # $1=file
|
||||
key=$(printf '%s' "$1" | cksum | tr ' \t' '--')
|
||||
off_f="$STATE/$cli-$key.offset"
|
||||
off=$(cat "$off_f" 2>/dev/null || echo 0)
|
||||
size=$(wc -c < "$1" 2>/dev/null || echo 0)
|
||||
[ "$size" -gt "$off" ] 2>/dev/null || return 0
|
||||
tail -c +"$((off + 1))" "$1" 2>/dev/null
|
||||
echo "$size" > "$off_f"
|
||||
}
|
||||
|
||||
# 從新增內容萃取錯誤,每筆一行 TSV:hook、event、exit、jsc、ts、detail。
|
||||
# 欄位用手寫掃描取,不靠正規式一次抓完:stderr 裡有轉義引號,正規式會抓過頭。
|
||||
extract_errors() {
|
||||
awk '
|
||||
# 取 JSON 字串或純量欄位;字串保留原本的轉義序列,寫回 jsonl 時才不必重新轉義。
|
||||
function jstr(s, name, p, i, c, out) {
|
||||
p = index(s, "\"" name "\":")
|
||||
if (p == 0) return ""
|
||||
i = p + length(name) + 3
|
||||
while (substr(s, i, 1) == " ") i++
|
||||
if (substr(s, i, 1) != "\"") {
|
||||
out = ""
|
||||
while (i <= length(s) && substr(s, i, 1) !~ /[,}\]]/) { out = out substr(s, i, 1); i++ }
|
||||
return out
|
||||
}
|
||||
i++
|
||||
out = ""
|
||||
while (i <= length(s)) {
|
||||
c = substr(s, i, 1)
|
||||
if (c == "\\") { out = out substr(s, i, 2); i += 2; continue }
|
||||
if (c == "\"") break
|
||||
out = out c; i++
|
||||
}
|
||||
return out
|
||||
}
|
||||
function is_jsc(t) {
|
||||
if (index(t, "ste100-guard.sh") || index(t, "session-timer.sh") \
|
||||
|| index(t, "skill-usage.sh") || index(t, "sdlc-gate.sh") \
|
||||
|| index(t, "version-guard.sh")) return "true"
|
||||
return "false"
|
||||
}
|
||||
# 摘要收斂成單行短字串:TSV 欄位不能有 tab,jsonl 欄位不能有裸換行;
|
||||
# 截斷可能切到半個轉義序列,尾端的反斜線要清掉才是合法 JSON 字串。
|
||||
function clean(t, x) {
|
||||
x = t
|
||||
gsub(/\t/, " ", x)
|
||||
gsub(/\\n/, " ", x)
|
||||
x = substr(x, 1, 300)
|
||||
sub(/\\+$/, "", x)
|
||||
if (x == "") x = "(無錯誤輸出)"
|
||||
return x
|
||||
}
|
||||
# hookErrors 的內容是 JSON 陣列原文,元素外面那對引號是結構、不是文字。
|
||||
# 直接寫進 jsonl 會多出一對裸引號把字串切斷,所以先拆成純文字,多筆用分號串起來。
|
||||
function unarray(a) {
|
||||
gsub(/","/, "; ", a)
|
||||
sub(/^"/, "", a)
|
||||
sub(/"$/, "", a)
|
||||
return a
|
||||
}
|
||||
function emit(hook, ev, ec, own, ts, detail) {
|
||||
if (hook == "") hook = "unknown"
|
||||
if (ev == "") ev = "-"
|
||||
if (ec == "") ec = "-"
|
||||
printf "%s\t%s\t%s\t%s\t%s\t%s\n", hook, ev, ec, own, ts, clean(detail)
|
||||
}
|
||||
/"type":"hook_non_blocking_error"/ {
|
||||
err = jstr($0, "stderr"); cmd = jstr($0, "command")
|
||||
detail = (err != "" ? err : cmd)
|
||||
emit(jstr($0, "hookName"), jstr($0, "hookEvent"), jstr($0, "exitCode"), \
|
||||
is_jsc(cmd " " err), jstr($0, "timestamp"), detail)
|
||||
next
|
||||
}
|
||||
# 非空的 hookErrors:只有訊息陣列,沒有 hook 名與退出碼,欄位據實留空。
|
||||
/"hookErrors":\[[^]]/ {
|
||||
p = index($0, "\"hookErrors\":[")
|
||||
rest = substr($0, p + length("\"hookErrors\":["))
|
||||
q = index(rest, "]")
|
||||
arr = (q > 1 ? substr(rest, 1, q - 1) : rest)
|
||||
emit("", "", "", is_jsc(arr), jstr($0, "timestamp"), unarray(arr))
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
d="$HOME/.claude/projects"
|
||||
if [ ! -d "$d" ]; then
|
||||
printf 'status=clean reason=%s\n' "找不到 $d,沒有 claude 紀錄可掃"
|
||||
echo "[jsc] claude:這台機器沒有 transcript 目錄,掃不到紀錄跟沒有錯誤是兩件事,請改跑 tools/wire-cli.sh smoke claude。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
recs=$(mktemp) || { printf 'status=clean reason=%s\n' "無法建立暫存檔"; exit 0; }
|
||||
files=$(mktemp) || { rm -f "$recs"; printf 'status=clean reason=%s\n' "無法建立暫存檔"; exit 0; }
|
||||
trap 'rm -f "$recs" "$files"' EXIT
|
||||
find "$d" -type f -name '*.jsonl' 2>/dev/null | sort > "$files"
|
||||
# 迴圈從檔案讀,不放在管線右邊:管線會開子 shell,計數與旗標傳不回本 shell。
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
new_content "$f" | extract_errors >> "$recs"
|
||||
done < "$files"
|
||||
|
||||
total=0; jsc_n=0
|
||||
human=$(mktemp) || { printf 'status=clean reason=%s\n' "無法建立暫存檔"; exit 0; }
|
||||
TAB=$(printf '\t')
|
||||
while IFS="$TAB" read -r hook ev ec isjsc ts detail; do
|
||||
[ -n "$hook" ] || continue
|
||||
[ -n "$ts" ] || ts=$(now_iso)
|
||||
total=$((total + 1))
|
||||
printf '{"ts":"%s","cli":"%s","hook":"%s","event":"%s","exit":"%s","detail":"%s","jsc":%s}\n' \
|
||||
"$ts" "$cli" "$hook" "$ev" "$ec" "$detail" "$isjsc" >> "$OUT"
|
||||
if [ "$isjsc" = true ]; then
|
||||
jsc_n=$((jsc_n + 1)); own="屬 jsc"
|
||||
else
|
||||
own="非 jsc 的第三方 hook"
|
||||
fi
|
||||
printf '[jsc] %s(%s 事件)exit %s,%s:%s\n' "$hook" "$ev" "$ec" "$own" "$detail" >> "$human"
|
||||
done < "$recs"
|
||||
|
||||
if [ "$total" -eq 0 ]; then
|
||||
rm -f "$human"
|
||||
printf 'status=clean reason=%s\n' "claude 紀錄裡沒有新的 hook 執行期錯誤"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf 'status=errors reason=%s\n' "掃到 $total 筆 hook 執行期錯誤,其中 $jsc_n 筆屬 jsc"
|
||||
cat "$human"
|
||||
rm -f "$human"
|
||||
echo "[jsc] 屬 jsc 的錯誤請先以 tools/report-error.sh 回報,再交給 /jsc-hooks:repair 自動修正並開 develop PR。"
|
||||
echo "[jsc] 非 jsc 的第三方 hook 錯誤只回報,不由 jsc 修正。"
|
||||
echo "[jsc] 全部紀錄已附加到 $OUT。"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user