異常回報依 wiki 讀取的結束碼分流,只有頁面確定不存在才套範本建新頁。 版本閘門與重啟閘門的豁免清單各補上 hook 修復技能。 階段閘門把計畫階段移出擋人名單,改成只注入提醒。 錯誤掃描的自家 hook 判定補齊九支腳本,並加一條路徑判定。 修復技能與接線技能的內文改成真的走得到的路徑與真的存在的關卡數。 原本 wiki 讀取失敗會一路落到套範本那一步,金鑰失效或 API 出狀況時, 就拿一份空白範本蓋掉整份異常目錄,而寫入不做合併也不留備份,蓋掉就救不回來。 兩道閘門把唯一的 hook 修復路徑一起擋住,hook 一壞就沒有任何方法修回來, 閘門等於鎖掉解除自己的路徑。工作包閘門擋下計畫階段是誤擋: 計畫是純邏輯階段、不碰程式碼,而閘門只知道有 PR 未合併,判不出跟新計畫有沒有關聯。 自家 hook 判定只認得早期那五支,後來加的四支出錯會被當成第三方的,只回報不修正。 修復技能裡三個指向流程的路徑指到不存在的位置,照著走一定撲空; 接線流程寫四道關卡,實際上有五道,兩段中文說明也混在英文內文裡。 異常目錄改成先讀回舊頁、把新列附在文末、再整頁寫回;讀不回來就放棄寫目錄頁並回報, 寧可少一列索引,也不覆蓋別人的紀錄。兩份豁免清單各補一項,理由逐項寫在腳本檔頭。 計畫階段改印提醒後放行,放棄的在製品上限與代價一併寫在檔頭。 自家 hook 判定逐支列出腳本名,再加一條安裝路徑判定,日後新增 hook 忘了補清單也還認得出來。 三個路徑改指到擁有它的技能組,關卡數改成五道並逐關寫明結束碼,兩段中文說明改回英文。 版本閘門在同一次改動另補唯讀的建議子命令,把版本比對表收斂成一行結論, 部署技能不必自己再解一次那張表。
197 lines
8.6 KiB
Bash
Executable File
197 lines
8.6 KiB
Bash
Executable File
#!/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 命中九支 hook 腳本任一支,或命中 jsc-hooks 路徑,就是 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
|
||
}
|
||
# 判定這筆錯誤是不是 jsc 自己的 hook。腳本名逐支列,再補一條 jsc-hooks 路徑判定:
|
||
# 接線寫進設定的命令一律走 $JSC_HOME/current/jsc-hooks,路徑本身就是證據,新增 hook 時
|
||
# 就算忘了補進下面這張清單也還認得出來。認錯邊的代價不對稱——漏認會把 jsc 的錯誤當成
|
||
# 第三方的,只回報不修正。
|
||
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") || index(t, "restart-gate.sh") \
|
||
|| index(t, "comment-scope.sh") || index(t, "lang-guard.sh") \
|
||
|| index(t, "write-guard.sh") || index(t, "jsc-hooks")) 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
|