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,就是那一輪中止了。
This commit is contained in:
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user