Files
hooks/tools/report-status.sh
jiantw83 a3ef205489 feat(狀態回報): 技能與 hook 的執行結果寫進本機事件流
現行紀錄只記「被叫用」,欄位是 ts、cli、session、skill,沒有成敗也沒有
結束碼。跑完整輪的技能與開場就中止的技能,在紀錄裡長得一模一樣。hook
成功時更是完全不留紀錄,只有錯誤路徑會寫 wiki,而那條路徑刻意不自動觸發。

事件流走本機檔案,不直接寫 wiki。hook 每次提示都跑,網路寫入會拖垮宿主
CLI;失敗的 hook 自我回報還會疊出迴圈,既有的錯誤回報因此不接在失敗的
hook 上,這裡沿用同一條線。助理巡檢時排空、彙整、寫頁。

hook 端用 EXIT trap 接,一支只加一行。這幾支的 exit 點很多,階段閘門一支
就有五十幾個;逐點改要動到每一條判定路徑,而那些路徑正是閘門的判準,為了
加一行紀錄去動閘門,風險遠大於收益。trap 涵蓋每一條離開路徑,含中途失敗。

狀態預設由結束碼推,推不出來的由 hook 自己覆寫。相依版本檢查與兩道閘門有
這種情形:antigravity 走 deny JSON、kiro 只印警告,兩者擋下時結束碼都是 0,
單看結束碼會把擋下記成放行。

技能的 start 由既有的技能用量 hook 順手發,不必改任何技能文件。end 只能由
技能自己在收尾步驟寫——hook 觸發時技能的實際工作還在後面的模型輪次,看不到
成敗。有 start 沒有配對的 end,就是那一輪中止了。
2026-09-02 15:40:17 +08:00

131 lines
5.6 KiB
Bash
Executable File
Raw Permalink 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
# report-status.sh — 技能與 hook 的執行狀態事件流。
#
# 為什麼要有這支:現行 usage/skills.jsonl 只記「被叫用」,欄位是 {ts,cli,session,skill},
# 沒有成敗、沒有結束碼。跑完整輪的技能與開場就中止的技能,在紀錄裡長得一模一樣。
# hook 成功時更是完全不留紀錄,只有錯誤路徑會寫 wiki,而那條路徑刻意不自動觸發。
#
# 為什麼不直接寫 wiki:hook 每次提示都跑,網路寫入會拖垮宿主 CLI;失敗的 hook 自我回報
# 還會疊出迴圈。所以一律先寫本機事件流,助理巡檢時排空、彙整、寫 MONITOR 頁。
#
# 用法:
# report-status.sh skill-start <名稱>
# report-status.sh skill-end <名稱> <status> [結束碼] [detail]
# report-status.sh hook-end <名稱> <status> <結束碼> [detail]
# report-status.sh drain # 印出上次排空之後的新事件
# report-status.sh rotate # 超過上限就輪替,只留一份舊的
#
# <名稱>: 技能寫 {domain}:{skill},hook 寫 {腳本檔名} 加子命令,例如 sdlc-gate check。
# <status>: ok、blocked、failed、degraded、aborted 五選一。
# ok 完成條件全部達成
# blocked 被閘門或前置條件擋下,沒有做事
# failed 做到一半失敗
# degraded 做完了但有部分沒達成
# aborted 使用者中止,或前提不成立而主動停止
#
# 規則:
# - 三個記錄子命令一律回 0,寫檔失敗也是 0。回報機制自己壞掉,不可以讓被回報的東西
# 跟著壞——hook 的結束碼是閘門的判準,被記錄動到就等於閘門行為被記錄改寫。
# 參數檢查是例外:那是呼叫端的程式錯誤,寫進去只會汙染事件流,所以先擋下來。
# - 本檔不讀 stdin。技能由 Bash 呼叫它,stdin 可能是還沒關閉的管線,讀下去會卡住宿主。
# 所有資訊一律走參數。
# - 輪替不放在每次寫入。每次提示都寫事件,順手 stat 一次檔案就是每次提示多一次系統呼叫;
# 改由巡檢排空之後呼叫 rotate,成本落在本來就週期性執行的地方。
#
# 結束碼: 0=成功(記錄子命令一律 0) 2=用法錯誤 3=drain 沒有新事件
set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
# lib.sh 在同一個存取庫,不是跨 plugin 依賴。hook 必須自足,事件寫入的函式因此留在 lib.sh,
# 這支只是把它包成命令列介面給技能用。
STDIN_JSON=""
. "$HERE/../hooks/lib.sh"
EVENTS="$JSC_HOME/usage/events.jsonl"
OFFSET="$JSC_HOME/usage/scan-state/events.offset"
MAX_BYTES=5242880
usage() {
cat >&2 <<'EOF'
用法:
report-status.sh skill-start <名稱>
report-status.sh skill-end <名稱> <status> [結束碼] [detail]
report-status.sh hook-end <名稱> <status> <結束碼> [detail]
report-status.sh drain
report-status.sh rotate
status: ok、blocked、failed、degraded、aborted
結束碼: 0=成功 2=用法錯誤 3=drain 沒有新事件
EOF
exit 2
}
valid_status() {
case "$1" in
ok|blocked|failed|degraded|aborted) ;;
*) echo "[jsc][狀態回報][ERR]:status 須為 ok、blocked、failed、degraded、aborted 五選一,收到「$1」。" >&2; exit 2 ;;
esac
}
valid_exit() {
case "$1" in
''|*[!0-9]*) echo "[jsc][狀態回報][ERR]:結束碼須為非負整數,收到「$1」。" >&2; exit 2 ;;
esac
}
cmd="${1:-}"; [ -n "$cmd" ] || usage
shift || true
case "$cmd" in
skill-start)
name="${1:-}"; [ -n "$name" ] || usage
# start 沒有成敗可言,狀態欄固定 ok、結束碼固定 0。判讀靠的是「有沒有配對的 end」:
# 有 start 沒 end 就是中止,那正是現行紀錄分不出來的那一種。
emit_event skill "$name" start ok 0
;;
skill-end)
name="${1:-}"; status="${2:-}"
[ -n "$name" ] && [ -n "$status" ] || usage
valid_status "$status"
code="${3:-0}"; valid_exit "$code"
emit_event skill "$name" end "$status" "$code" "" "${4:-}"
;;
hook-end)
name="${1:-}"; status="${2:-}"; code="${3:-}"
[ -n "$name" ] && [ -n "$status" ] && [ -n "$code" ] || usage
valid_status "$status"; valid_exit "$code"
emit_event hook "$name" end "$status" "$code" "" "${4:-}"
;;
drain)
[ -f "$EVENTS" ] || exit 3
size=$(wc -c < "$EVENTS" 2>/dev/null || echo 0)
old=0
[ -f "$OFFSET" ] && old=$(cat "$OFFSET" 2>/dev/null || echo 0)
case "$old" in ''|*[!0-9]*) old=0 ;; esac
# 檔案比已存位移還小就是輪替過,從頭讀。不比對 inode:五支 CLI 與容器裡的行程
# 看到的 inode 不保證一致,用大小判斷才在每個環境都成立。
[ "$size" -lt "$old" ] && old=0
[ "$size" -eq "$old" ] && exit 3
mkdir -p "$(dirname "$OFFSET")" 2>/dev/null || true
# tail -c +N 從第 N 個位元組起(1 起算),所以位移要加一。
# 不用 dd bs=1 skip=:那是一個位元組一次系統呼叫,位移到了幾 MB 就是幾百萬次,
# 每輪巡檢都排空一次的話會慢到不能用。
tail -c "+$((old + 1))" "$EVENTS" 2>/dev/null || true
printf '%s' "$size" > "$OFFSET" 2>/dev/null || true
;;
rotate)
[ -f "$EVENTS" ] || exit 0
size=$(wc -c < "$EVENTS" 2>/dev/null || echo 0)
if [ "$size" -gt "$MAX_BYTES" ]; then
mv "$EVENTS" "$EVENTS.1" 2>/dev/null || true
: > "$EVENTS" 2>/dev/null || true
# 位移歸零:新檔從頭算起,不歸零的話下一次 drain 會跳過開頭那一段。
printf '0' > "$OFFSET" 2>/dev/null || true
printf '已輪替:%s -> %s.1(原大小 %s 位元組)\n' "$EVENTS" "$EVENTS" "$size"
fi
;;
*) usage ;;
esac
exit 0